对于类类型,可以分配实际上不允许内置类型的临时对象.此外,默认生成的赋值运算符甚至会产生左值:
int() = int(); // illegal: "expression is not assignable" struct B {}; B& b = B() = B(); // compiles OK: yields an lvalue! ... but is wrong! (see below)
对于最后一个语句,赋值运算符的结果实际上用于初始化非const
引用,该引用将在语句之后立即变为陈旧:引用未直接绑定到临时对象(它不能作为临时对象只能是绑定到一个const
或右值引用)但绑定到其生命周期未延长的赋值结果.
另一个问题是从赋值运算符返回的左值看起来不像它可以被移动,尽管它实际上是指临时的.如果有任何东西使用赋值的结果来获取值,它将被复制而不是移动,尽管移动是完全可行的.此时值得注意的是,问题是根据赋值运算符描述的,因为此运算符通常可用于值类型并返回左值引用.任何返回对象引用的函数都存在同样的问题,即*this
.
一个潜在的解决方法是重载赋值运算符(或返回对象引用的其他函数)以考虑对象的类型,例如:
class G { public: // other members G& operator=(G) & { /*...*/ return *this; } G operator=(G) && { /*...*/ return std::move(*this); } };
C++ 11提供了如上所述重载赋值运算符的可能性,并且可以防止上面提到的细微对象失效并同时允许将赋值结果移动到临时值.这两个运营商的实施可能完全相同.虽然实现可能相当简单(基本上只是swap()
两个对象中的一个),但它仍然意味着提出问题的额外工作:
返回对象的引用的函数(例如,赋值运算符)是否应该观察被赋值对象的右值?
另外一个(在评论中由Simple提到)是不重载赋值运算符,但是使用a &
来限制它的使用到lvalues:
class GG { public: // other members GG& operator=(GG) & { /*...*/ return *this; } }; GG g; g = GG(); // OK GG() = GG(); // ERROR
Cassio Neri.. 5
恕我直言,DietmarKühl的原始建议(提供重载&
和&&
参考资格)优于Simple的(仅提供它&
).最初的想法是:
class G { public: // other members G& operator=(G) & { /*...*/ return *this; } G operator=(G) && { /*...*/ return std::move(*this); } };
和Simple建议删除第二个重载.两种解决方案都无效
G& g = G() = G();
(如果需要),但如果第二个重载被删除,那么这些行也无法编译:
const G& g1 = G() = G(); G&& g2 = G() = G();
而且我认为他们没有理由不这样做(Yakk的帖子中没有解释终生问题).
我只能看到一种情况,其中Simple的建议更可取:什么时候G
没有可访问的复制/移动构造函数.由于可以访问复制/移动赋值运算符的大多数类型也具有可访问的复制/移动构造函数,因此这种情况非常罕见.
两个重载都是按值获取参数,如果G
有一个可访问的复制/移动构造函数,则有很好的理由.假设现在是G
它不具备一个.在这种情况下,运算符应该采用参数const G&
.
不幸的是,第二个重载(实际上是按值返回)不应该返回一个引用(任何类型),*this
因为*this
绑定到的表达式是一个rvalue因此,它可能是一个临时的,其生命周期即将到来到期.(回想一下,禁止这种情况发生是OP的动机之一.)
在这种情况下,您应该删除第二个重载(根据Simple的建议)否则该类不编译(除非第二个重载是一个从未实例化的模板).或者,我们可以保留第二个重载并将其定义为delete
d.(但是,为什么&
单独存在过载已经足够了?)
外围点.
什么应该是operator =
for 的定义&&
?(我们再次假设G
有一个可访问的复制/移动构造函数.)
作为迪特马尔·库尔指出和Yakk探索了,两个重载的代码应该是非常相似的,在这种情况下,最好是实施一个&&
在一个方面&
.由于移动的表现预计不会比副本差(并且由于RVO在返回时不适用*this
)我们应该返回std::move(*this)
.总之,可能的一行定义是:
G operator =(G o) && { return std::move(*this = std::move(o)); }
如果只能G
将其分配给另一个G
或G
具有(非显式)转换构造函数,那么这就足够了.否则,您应该考虑G
使用(模板)转发复制/移动赋值运算符来获取通用引用:
templateG operator =(T&& o) && { return std::move(*this = std::forward (o)); }
虽然这不是很多锅炉板代码,但如果我们必须为许多类别做这件事,那仍然是一个烦恼.为了减少锅炉板代码的数量,我们可以定义一个宏:
#define ASSIGNMENT_FOR_RVALUE(type) \ template\ type operator =(T&& b) && { return std::move(*this = std::forward (b)); }
然后在里面G
的定义中补充道ASSIGNMENT_FOR_RVALUE(G)
.
(注意,有关类型的仅出现作为返回类型,在C++ 14它可以通过编译器自动推断,因此,G
和type
在最后两个代码段可以被替换auto
.由此可见,宏可以成为一个对象类似宏而不是像函数一样的宏.)
减少的锅炉板代码量的另一种方式是定义一个实现一个CRTP基类operator =
为&&
:
templatestruct assignment_for_rvalue { template Derived operator =(T&& o) && { return std::move(static_cast (*this) = std::forward (o)); } };
锅炉板成为继承和使用声明,如下所示:
class G : public assignment_for_rvalue{ public: // other members, possibly including assignment operator overloads for `&` // but taking arguments of different types and/or value category. G& operator=(G) & { /*...*/ return *this; } using assignment_for_rvalue::operator =; };
回想一下,对于某些类型而且与使用相反ASSIGNMENT_FOR_RVALUE
,继承assignment_for_rvalue
可能会对类布局产生一些不良后果.
恕我直言,DietmarKühl的原始建议(提供重载&
和&&
参考资格)优于Simple的(仅提供它&
).最初的想法是:
class G { public: // other members G& operator=(G) & { /*...*/ return *this; } G operator=(G) && { /*...*/ return std::move(*this); } };
和Simple建议删除第二个重载.两种解决方案都无效
G& g = G() = G();
(如果需要),但如果第二个重载被删除,那么这些行也无法编译:
const G& g1 = G() = G(); G&& g2 = G() = G();
而且我认为他们没有理由不这样做(Yakk的帖子中没有解释终生问题).
我只能看到一种情况,其中Simple的建议更可取:什么时候G
没有可访问的复制/移动构造函数.由于可以访问复制/移动赋值运算符的大多数类型也具有可访问的复制/移动构造函数,因此这种情况非常罕见.
两个重载都是按值获取参数,如果G
有一个可访问的复制/移动构造函数,则有很好的理由.假设现在是G
它不具备一个.在这种情况下,运算符应该采用参数const G&
.
不幸的是,第二个重载(实际上是按值返回)不应该返回一个引用(任何类型),*this
因为*this
绑定到的表达式是一个rvalue因此,它可能是一个临时的,其生命周期即将到来到期.(回想一下,禁止这种情况发生是OP的动机之一.)
在这种情况下,您应该删除第二个重载(根据Simple的建议)否则该类不编译(除非第二个重载是一个从未实例化的模板).或者,我们可以保留第二个重载并将其定义为delete
d.(但是,为什么&
单独存在过载已经足够了?)
外围点.
什么应该是operator =
for 的定义&&
?(我们再次假设G
有一个可访问的复制/移动构造函数.)
作为迪特马尔·库尔指出和Yakk探索了,两个重载的代码应该是非常相似的,在这种情况下,最好是实施一个&&
在一个方面&
.由于移动的表现预计不会比副本差(并且由于RVO在返回时不适用*this
)我们应该返回std::move(*this)
.总之,可能的一行定义是:
G operator =(G o) && { return std::move(*this = std::move(o)); }
如果只能G
将其分配给另一个G
或G
具有(非显式)转换构造函数,那么这就足够了.否则,您应该考虑G
使用(模板)转发复制/移动赋值运算符来获取通用引用:
template <typename T> G operator =(T&& o) && { return std::move(*this = std::forward<T>(o)); }
虽然这不是很多锅炉板代码,但如果我们必须为许多类别做这件事,那仍然是一个烦恼.为了减少锅炉板代码的数量,我们可以定义一个宏:
#define ASSIGNMENT_FOR_RVALUE(type) \ template <typename T> \ type operator =(T&& b) && { return std::move(*this = std::forward<T>(b)); }
然后在里面G
的定义中补充道ASSIGNMENT_FOR_RVALUE(G)
.
(注意,有关类型的仅出现作为返回类型,在C++ 14它可以通过编译器自动推断,因此,G
和type
在最后两个代码段可以被替换auto
.由此可见,宏可以成为一个对象类似宏而不是像函数一样的宏.)
减少的锅炉板代码量的另一种方式是定义一个实现一个CRTP基类operator =
为&&
:
template <typename Derived> struct assignment_for_rvalue { template <typename T> Derived operator =(T&& o) && { return std::move(static_cast<Derived&>(*this) = std::forward<T>(o)); } };
锅炉板成为继承和使用声明,如下所示:
class G : public assignment_for_rvalue<G> { public: // other members, possibly including assignment operator overloads for `&` // but taking arguments of different types and/or value category. G& operator=(G) & { /*...*/ return *this; } using assignment_for_rvalue::operator =; };
回想一下,对于某些类型而且与使用相反ASSIGNMENT_FOR_RVALUE
,继承assignment_for_rvalue
可能会对类布局产生一些不良后果.