c++ - 赋值运算符是否应该观察被赋值对象的右值?

标签 c++ c++11 rvalue-reference

对于类类型,可以分配给内置类型实际上不允许的临时对象。此外,默认生成的赋值运算符甚至会产生一个左值:

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();

并且我认为他们没有理由不这样做(没有在 Yakkpost 中解释的生命周期问题)。

我只能看到一种情况,即 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 中,编译器可以自动推断出它,因此,最后两个代码片段中的 Gtype 可以替换为 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/

相关文章:

c++ - 从具有特定字符串长度的 vector 中删除元素

C++ 为什么在定义重载和引用函数时每个函数都应该有一个引用限定符

c++ - c++ 中的右值如何存储在内存中?

c++ - 如何使用QWebEnginePage::OpenLinkInNewTab [Qt5.8]

c++ - C 和 C++ 中 printf ("%d\n"、 sizeof ('a' )) 的结果是什么

c++ - 如何配置 VS 2010 来创建一个利用其他 DLL 的 DLL

c++11 - 为什么 std::aligned_union 需要最小尺寸作为模板参数?

c++ - 使用 constexpr 编译时间字符串加密

c++ - Const Rvalue 引用以捕获不应编译的重载

c++ - shared_ptr的 setter 功能