对于类类型,可以分配给内置类型实际上不允许的临时对象。此外,默认生成的赋值运算符甚至会产生一个左值:
int() = int(); // illegal: "expression is not assignable"
struct B {};
B& b = B() = B(); // compiles OK: yields an lvalue! ... but is wrong! (see below)
对于最后一条语句,赋值运算符的结果实际上用于初始化一个非
const
将在语句之后立即失效的引用:引用不直接绑定(bind)到临时对象(它不能因为临时对象只能绑定(bind)到 const
或右值引用)而是绑定(bind)到赋值的结果生命周期没有延长。另一个问题是赋值运算符返回的左值看起来好像不能移动,尽管它实际上是指临时的。如果有任何东西使用赋值的结果来获取值,它将被复制而不是移动,尽管移动完全可行。在这一点上值得注意的是,问题是根据赋值运算符来描述的,因为该运算符通常可用于值类型并返回左值引用。任何返回对象引用的函数都存在同样的问题,即
*this
.一个潜在的解决方法是重载赋值运算符(或其他返回对象引用的函数)以考虑对象的类型,例如:
class G {
public:
// other members
G& operator=(G) & { /*...*/ return *this; }
G operator=(G) && { /*...*/ return std::move(*this); }
};
C++11 提供了重载赋值运算符的可能性,可以防止上面提到的微妙的对象失效,同时允许将赋值的结果移动到临时对象。这两个运算符的实现可能是相同的。尽管实现可能相当简单(基本上只是两个对象的
swap()
),但它仍然意味着额外的工作提出了这个问题:返回对象引用的函数(例如,赋值运算符)是否应该观察被赋值对象的右值?
另一种方法(由 Simple 在评论中提到)是不重载赋值运算符,而是用
&
明确限定它。将其使用限制为左值:class GG {
public:
// other members
GG& operator=(GG) & { /*...*/ return *this; }
};
GG g;
g = GG(); // OK
GG() = GG(); // ERROR
最佳答案
恕我直言,最初的建议来自 Dietmar Kü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 的 post 中解释的生命周期问题)。
我只能看到一种情况,即 Simple的建议更可取:当
G
没有可访问的复制/移动构造函数。由于大多数可访问复制/移动赋值运算符的类型也具有可访问的复制/移动构造函数,因此这种情况非常罕见。两个重载都按值获取参数,如果
G
有充分的理由这样做有一个可访问的复制/移动构造函数。现在假设 G
没有一个。在这种情况下,运算符(operator)应该通过 const G&
来获取参数。 .不幸的是,第二个重载(按值返回)不应该返回对
*this
的引用(任何类型)。因为 *this
的表达式binds to 是一个右值,因此它很可能是一个生命周期即将到期的临时对象。 (回想一下,禁止这种情况发生是 OP 的动机之一。)在这种情况下,您应该删除第二个重载(根据 Simple 的建议),否则该类不会编译(除非第二个重载是从未实例化的模板)。或者,我们可以保留第二个重载并将其定义为
delete
d. (但是为什么要麻烦,因为仅 &
的过载就已经足够了?)一个外围点。
operator =
的定义应该是什么为 &&
? (我们再次假设 G
有一个可访问的复制/移动构造函数。)如 Dietmar Kühl已指出和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
。因此,宏可以变成类对象的宏,而不是类函数的宏。)另一种减少样板代码量的方法是定义一个实现
operator =
的 CRTP 基类。为 &&
: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
可能会对类布局产生一些不必要的后果。
关于c++ - 赋值运算符是否应该观察被赋值对象的右值?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20886466/