如何在 C++ 中使用移动语义进行运算符重载? (优雅地)
How to do Operator overloading with move semantics in c++? (Elegantly)
class T {
size_t *pData; // Memory allocated in the constructor
friend T operator+(const T& a, const T& b);
};
T operator+(const T& a, const T& b){ // Op 1
T c; // malloc()
*c.pData = *a.pData + *b.pData;
return c;
}
T do_something(){
/* Implementation details */
return T_Obj;
}
带有动态内存的简单class T
。考虑
T a,b,c;
c = a + b; // Case 1
c = a + do_something(b); // Case 2
c = do_something(a) + b; // Case 3
c = do_something(a) + do_something(b); // Case 4
- 案例 1 使用 1 个 malloc()
- 案例 2 使用 2 个 malloc()
- 案例 3 使用 2 个 malloc()
- 案例 4 使用 3 个 malloc()
我们可以通过额外定义来做得更好,
T& operator+(const T& a, T&& b){ // Op 2
// no malloc() steeling data from b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
Case 2现在只使用了1个malloc(),那么Case 3呢?我们需要定义 Op 3 吗?
T& operator+(T&& a, const T& b){ // Op 3
// no malloc() steeling data from a rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
此外,如果我们确实定义了 Op 2 和 Op 3,鉴于右值引用可以绑定到左值引用这一事实,编译器现在有两个同样合理的函数定义可以在案例 4 中调用
T& operator+(const T& a, T&& b); // Op 2 rvalue binding to a
T& operator+(T&& a, const T& b); // Op 3 rvalue binding to b
编译器会抱怨函数调用不明确,定义 Op 4 是否有助于解决编译器的函数调用不明确问题?因为我们没有通过 Op 4
获得额外的性能
T& operator+(T&& a, T&& b){ // Op 4
// no malloc() can steel data from a or b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
对于 Op 1、Op 2、Op 3 和 Op 4,我们有
- 情况 1:1 个 malloc(调用 Op 1)
- 情况 2:1 个 malloc(调用 Op 2)
- 情况 3:1 个 malloc(调用 Op 3)
- 案例 4:1 个 malloc(调用 Op 4)
如果我的理解是正确的,我们将需要每个运算符四个函数签名。这在某种程度上似乎不对,因为每个操作员都有很多样板和代码重复。我错过了什么吗?有没有一种优雅的方法可以达到同样的效果?
最好不要尝试使用 operator+
(或任何二元运算符)窃取资源并设计一个更合适的可以以某种方式重用数据的方法1.这应该是您 API 惯用的构建方式,即使不是唯一的方式(如果您想完全避免该问题)。
C++ 中的二元运算符,如 operator+
具有一般性 expectation/convention,它 returns 是一个不同的对象 ,而不改变其任何输入 。定义一个 operator+
以除左值外还与右值一起操作引入了一个非常规的接口,这将引起大多数 C++ 开发人员的困惑。
考虑您的 案例 4 示例:
c = do_something(a) + do_something(b); // Case 4
哪个资源被盗了,a
还是b
?如果 a
也不够大以支持 b
所需的结果怎么办(假设这使用了大小调整缓冲区)?没有一般情况可以使它成为一个简单的解决方案。
此外,无法区分 API 上不同类型的右值,例如 Xvalues(std::move
的结果)和 PRvalues(returns 函数的结果)一个值)。这意味着您可以调用相同的 API:
c = std::move(a) + std::move(b);
在这种情况下,根据您的上述启发式,只有 a
或 b
之一的资源可能被盗,这很奇怪.这将导致底层资源的生命周期 未被延长 至 c
,这可能违背开发人员的直觉(例如,如果 [=16 中的资源=] 或 b
具有可观察的 side-effects,如日志记录或其他系统交互)
注意:值得注意的是,C++中的std::string
也有同样的问题,其中operator+
效率低下。重用缓冲区的一般建议是在这种情况下使用 operator+=
1 解决此类问题的更好方法是以某种方式创建一种正确的构建方法,并始终如一地使用它。这可以通过 well-named 函数,某种适当的 builder
class,或者只是使用像 operator+=
这样的复合运算符
这甚至可以通过将一系列参数折叠成 +=
串联系列的模板辅助函数来完成。假设这是在 c++17 或以上,这可以很容易地完成:
template <typename...Args>
auto concat(Args&&...args) -> SomeType
{
auto result = SomeType{}; // assuming default-constructible
(result += ... += std::forward<Args>(args));
return result;
}
技术上是可行的。但也许您应该考虑更改设计。
该代码只是一个 POC。
它有一个 UB,但它适用于 gcc 和 clang...
#include <type_traits>
#include <iostream>
struct T {
T()
: pData (new size_t(1))
, owner(true)
{
std::cout << "malloc" << std::endl;
}
~T()
{
if (owner)
{
delete pData;
}
}
T(const T &) = default;
size_t *pData; // Memory allocated in the constructor
bool owner; // pData ownership
template <class T1, class T2>
friend T operator+(T1 && a, T2 && b){
T c(std::forward<T1>(a), std::forward<T2>(b));
*c.pData = *a.pData + *b.pData; //UB but works
return c;
}
private:
template <class T1, class T2>
T(T1 && a, T2 && b) : owner(true)
{
static_assert(std::is_same_v<T, std::decay_t<T1>> && std::is_same_v<T, std::decay_t<T2>>, "only type T is supported");
if (!std::is_reference<T1>::value)
{
pData = a.pData;
a.owner = false;
std::cout << "steal data a" << std::endl;
}
else if (!std::is_reference<T2>::value)
{
pData = b.pData;
b.owner = false;
std::cout << "steal data b" << std::endl;
}
else
{
std::cout << "malloc anyway" << std::endl;
pData = new size_t(0);
}
}
};
int main()
{
T a, b;
T r = a +b; // malloc
std::cout << *r.pData << std::endl;
T r2 = std::move(a) + b; // no malloc
std::cout << *r2.pData << " a: " << *a.pData << std::endl;
T r3 = a + std::move(b); // no malloc
std::cout << *r3.pData << " a: " << *a.pData << " b: " << *b.pData << std::endl;
return 0;
}
这是高效且优雅的,但使用了宏。
#include <type_traits>
#include <iostream>
#define OPERATOR_Fn(Op) \
template<typename T1, typename T2> \
friend auto operator Op (T1&& a, T2&& b) \
-> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type \
{ \
constexpr bool a_or_b = !std::is_reference<T1>::value; \
std::decay_t<T1> c((a_or_b? std::forward<T1>(a) : std::forward<T2>(b))); \
\
*c.pData = *c.pData Op (!a_or_b? *a.pData : *b.pData); \
return c; \
} \
struct T {
T(): pData(new size_t(1)) {std::cout << "malloc" << '\n';}
~T() {delete pData;}
T(const T& b): pData(new size_t(1)) { *pData = *b.pData; std::cout << "malloc" << '\n';}
T(T&& b){
pData = b.pData;
b.pData = nullptr;
std::cout<< "move constructing" << '\n';
}
size_t *pData; // Memory allocated in the constructor
OPERATOR_Fn(+);
OPERATOR_Fn(-);
OPERATOR_Fn(&);
OPERATOR_Fn(|);
};
您可以简化 type_traits 表达式,通过定义类似这样的内容来提高代码的可读性
template <typename T1, typename T2>
struct enable_if_same_on_decay{
static constexpr bool value = std::is_same<std::decay_t<T1>, std::decay_t<T2>>::value;
typedef std::enable_if<value,std::decay_t<T>>::type type;
};
template <typename T1, typename T2>
using enable_if_same_on_decay_t = typename enable_if_same_on_decay<T1,T2>::type;
复杂的type_traits表达式
-> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type
就变成了
-> enable_if_same_on_decay_t<T1,T2>
class T {
size_t *pData; // Memory allocated in the constructor
friend T operator+(const T& a, const T& b);
};
T operator+(const T& a, const T& b){ // Op 1
T c; // malloc()
*c.pData = *a.pData + *b.pData;
return c;
}
T do_something(){
/* Implementation details */
return T_Obj;
}
带有动态内存的简单class T
。考虑
T a,b,c;
c = a + b; // Case 1
c = a + do_something(b); // Case 2
c = do_something(a) + b; // Case 3
c = do_something(a) + do_something(b); // Case 4
- 案例 1 使用 1 个 malloc()
- 案例 2 使用 2 个 malloc()
- 案例 3 使用 2 个 malloc()
- 案例 4 使用 3 个 malloc()
我们可以通过额外定义来做得更好,
T& operator+(const T& a, T&& b){ // Op 2
// no malloc() steeling data from b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
Case 2现在只使用了1个malloc(),那么Case 3呢?我们需要定义 Op 3 吗?
T& operator+(T&& a, const T& b){ // Op 3
// no malloc() steeling data from a rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
此外,如果我们确实定义了 Op 2 和 Op 3,鉴于右值引用可以绑定到左值引用这一事实,编译器现在有两个同样合理的函数定义可以在案例 4 中调用
T& operator+(const T& a, T&& b); // Op 2 rvalue binding to a
T& operator+(T&& a, const T& b); // Op 3 rvalue binding to b
编译器会抱怨函数调用不明确,定义 Op 4 是否有助于解决编译器的函数调用不明确问题?因为我们没有通过 Op 4
获得额外的性能T& operator+(T&& a, T&& b){ // Op 4
// no malloc() can steel data from a or b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
对于 Op 1、Op 2、Op 3 和 Op 4,我们有
- 情况 1:1 个 malloc(调用 Op 1)
- 情况 2:1 个 malloc(调用 Op 2)
- 情况 3:1 个 malloc(调用 Op 3)
- 案例 4:1 个 malloc(调用 Op 4)
如果我的理解是正确的,我们将需要每个运算符四个函数签名。这在某种程度上似乎不对,因为每个操作员都有很多样板和代码重复。我错过了什么吗?有没有一种优雅的方法可以达到同样的效果?
最好不要尝试使用 operator+
(或任何二元运算符)窃取资源并设计一个更合适的可以以某种方式重用数据的方法1.这应该是您 API 惯用的构建方式,即使不是唯一的方式(如果您想完全避免该问题)。
C++ 中的二元运算符,如 operator+
具有一般性 expectation/convention,它 returns 是一个不同的对象 ,而不改变其任何输入 。定义一个 operator+
以除左值外还与右值一起操作引入了一个非常规的接口,这将引起大多数 C++ 开发人员的困惑。
考虑您的 案例 4 示例:
c = do_something(a) + do_something(b); // Case 4
哪个资源被盗了,a
还是b
?如果 a
也不够大以支持 b
所需的结果怎么办(假设这使用了大小调整缓冲区)?没有一般情况可以使它成为一个简单的解决方案。
此外,无法区分 API 上不同类型的右值,例如 Xvalues(std::move
的结果)和 PRvalues(returns 函数的结果)一个值)。这意味着您可以调用相同的 API:
c = std::move(a) + std::move(b);
在这种情况下,根据您的上述启发式,只有 a
或 b
之一的资源可能被盗,这很奇怪.这将导致底层资源的生命周期 未被延长 至 c
,这可能违背开发人员的直觉(例如,如果 [=16 中的资源=] 或 b
具有可观察的 side-effects,如日志记录或其他系统交互)
注意:值得注意的是,C++中的std::string
也有同样的问题,其中operator+
效率低下。重用缓冲区的一般建议是在这种情况下使用 operator+=
1 解决此类问题的更好方法是以某种方式创建一种正确的构建方法,并始终如一地使用它。这可以通过 well-named 函数,某种适当的 builder
class,或者只是使用像 operator+=
这甚至可以通过将一系列参数折叠成 +=
串联系列的模板辅助函数来完成。假设这是在 c++17 或以上,这可以很容易地完成:
template <typename...Args>
auto concat(Args&&...args) -> SomeType
{
auto result = SomeType{}; // assuming default-constructible
(result += ... += std::forward<Args>(args));
return result;
}
技术上是可行的。但也许您应该考虑更改设计。 该代码只是一个 POC。 它有一个 UB,但它适用于 gcc 和 clang...
#include <type_traits>
#include <iostream>
struct T {
T()
: pData (new size_t(1))
, owner(true)
{
std::cout << "malloc" << std::endl;
}
~T()
{
if (owner)
{
delete pData;
}
}
T(const T &) = default;
size_t *pData; // Memory allocated in the constructor
bool owner; // pData ownership
template <class T1, class T2>
friend T operator+(T1 && a, T2 && b){
T c(std::forward<T1>(a), std::forward<T2>(b));
*c.pData = *a.pData + *b.pData; //UB but works
return c;
}
private:
template <class T1, class T2>
T(T1 && a, T2 && b) : owner(true)
{
static_assert(std::is_same_v<T, std::decay_t<T1>> && std::is_same_v<T, std::decay_t<T2>>, "only type T is supported");
if (!std::is_reference<T1>::value)
{
pData = a.pData;
a.owner = false;
std::cout << "steal data a" << std::endl;
}
else if (!std::is_reference<T2>::value)
{
pData = b.pData;
b.owner = false;
std::cout << "steal data b" << std::endl;
}
else
{
std::cout << "malloc anyway" << std::endl;
pData = new size_t(0);
}
}
};
int main()
{
T a, b;
T r = a +b; // malloc
std::cout << *r.pData << std::endl;
T r2 = std::move(a) + b; // no malloc
std::cout << *r2.pData << " a: " << *a.pData << std::endl;
T r3 = a + std::move(b); // no malloc
std::cout << *r3.pData << " a: " << *a.pData << " b: " << *b.pData << std::endl;
return 0;
}
这是高效且优雅的,但使用了宏。
#include <type_traits>
#include <iostream>
#define OPERATOR_Fn(Op) \
template<typename T1, typename T2> \
friend auto operator Op (T1&& a, T2&& b) \
-> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type \
{ \
constexpr bool a_or_b = !std::is_reference<T1>::value; \
std::decay_t<T1> c((a_or_b? std::forward<T1>(a) : std::forward<T2>(b))); \
\
*c.pData = *c.pData Op (!a_or_b? *a.pData : *b.pData); \
return c; \
} \
struct T {
T(): pData(new size_t(1)) {std::cout << "malloc" << '\n';}
~T() {delete pData;}
T(const T& b): pData(new size_t(1)) { *pData = *b.pData; std::cout << "malloc" << '\n';}
T(T&& b){
pData = b.pData;
b.pData = nullptr;
std::cout<< "move constructing" << '\n';
}
size_t *pData; // Memory allocated in the constructor
OPERATOR_Fn(+);
OPERATOR_Fn(-);
OPERATOR_Fn(&);
OPERATOR_Fn(|);
};
您可以简化 type_traits 表达式,通过定义类似这样的内容来提高代码的可读性
template <typename T1, typename T2>
struct enable_if_same_on_decay{
static constexpr bool value = std::is_same<std::decay_t<T1>, std::decay_t<T2>>::value;
typedef std::enable_if<value,std::decay_t<T>>::type type;
};
template <typename T1, typename T2>
using enable_if_same_on_decay_t = typename enable_if_same_on_decay<T1,T2>::type;
复杂的type_traits表达式
-> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type
就变成了
-> enable_if_same_on_decay_t<T1,T2>