赋值运算符是否应该观察指定对象的右值?

时间:2014-01-02 15:46:01

标签: 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引用,该引用将在语句之后立即变为陈旧:引用未直接绑定到临时对象(它可以“临时对象只能绑定到const或右值引用”,而是绑定到生命期不延长的赋值结果。

另一个问题是从赋值运算符返回的左值看起来不像是可以移动,尽管它实际上是指临时值。如果有任何东西使用赋值的结果来获取值,它将被复制而不是被移动,尽管移动是完全可行的。此时值得注意的是,问题是根据赋值运算符描述的,因为此运算符通常可用于值类型并返回左值引用。任何返回对象引用的函数都存在同样的问题,即*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

2 个答案:

答案 0 :(得分:5)

恕我直言,Dietmar Kühl的原始建议(为&&&参考限定符提供重载)优于Simple一个(仅提供给Simple {1}})。 最初的想法是:

&

Yakk建议删除第二个重载。两种解决方案都使此行无效

class G {
public:
    // other members
    G& operator=(G) &  { /*...*/ return *this; }
    G  operator=(G) && { /*...*/ return std::move(*this); }
};

(如所希望的那样)但是如果删除了第二个重载,那么这些行也无法编译:

G& g = G() = G();

我认为没有理由不这样做(postSimple中没有解释生命周期问题。)

我只能看到一种情况,Simple的建议更可取:当const G& g1 = G() = G(); G&& g2 = G() = G(); 没有可访问的复制/移动构造函数时。由于可以访问复制/移动赋值运算符的大多数类型也具有可访问的复制/移动构造函数,因此这种情况非常罕见。

两个重载都是按值获取参数,如果G具有可访问的复制/移动构造函数,则有充分的理由。假设现在G 有一个。在这种情况下,运算符应该通过G

来获取参数

不幸的是,第二个重载(实际上是按值返回)不应该将引用(任何类型)返回到const G&,因为*this绑定到的表达式是一个右值,因此,它可能是一个临时的,其寿命即将到期。 (回想一下,禁止这种情况发生是OP的动机之一。)

在这种情况下,您应该删除第二个重载(根据Dietmar Kühl的建议)否则该类不编译(除非第二个重载是一个从未实例化的模板)。或者,我们可以保留第二个重载并将其定义为*this d。 (但是,为什么单独存在delete的过载已经足够了呢?

外围点。

& operator =的定义应该是什么? (我们再次假设&&具有可访问的复制/移动构造函数。)

正如Yakk指出并且{{3}}已经探讨过,两个重载的代码应该非常相似,在这种情况下,最好为G实现一个&&的条款。由于移动的性能预期不比副本差(并且由于RVO在返回&时不适用),我们应该返回*this。总之,可能的单行定义是:

std::move(*this)

如果只能将G operator =(G o) && { return std::move(*this = std::move(o)); } 分配给另一个GG具有(非显式)转换构造函数,那么这已经足够了。否则,您应该考虑给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

(请注意,相关类型仅作为返回类型出现。在C ++ 14中,它可以由编译器自动推断,因此,最后两个代码段中的ASSIGNMENT_FOR_RVALUE(G)G可以由type替换。因此宏可以变成类似于对象的宏而不是类似函数的宏。)

减少样板代码量的另一种方法是定义一个CRTP基类,为auto实现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可能会对类布局产生一些不必要的后果。

答案 1 :(得分:3)

第一个问题是在C ++ 03中实际上并不正常:

B& b = B() = B();

,一旦该行完成,b就会绑定一个过期的临时值。

使用它的唯一“安全”方法是在函数调用中:

void foo(B&);
foo( B()=B() );

或类似的东西,临时的线寿命足以满足我们绑定它的生命周期。

我们可以用以下代码替换可能效率低下的B()=B()语法:

template<typename T>
typename std::decay<T>::type& to_lvalue( T&& t ) { return t; }

现在呼叫看起来更清晰了:

foo( to_lvalue(B()) );

通过纯铸造来做到这一点。生命周期仍未延长(我无法想到管理它的方法),但我们不构造对象然后毫无意义地将它们分配给另一个。

所以现在我们坐下来检查这两个选项:

G operator=(G o) && { return std::move(o); }
G&&  operator=(G o) && { *this = std::move(o); return std::move(*this); }
G  operator=(G o) && { *this = std::move(o); return std::move(*this); }

作为一个旁边的完整实现,假设存在G& operator=(G o)&并且写得正确。 (为什么在不需要时重复代码?)

第一个和第三个允许终身延长返回值,第二个使用*this的生命周期。第二个和第三个修改*this,而第一个修改*this

我会声称第一个是正确的答案。因为*this绑定了一个右值,所以调用者声明它不会被重用,并且它的状态无关紧要:改变它是没有意义的。

第一个和第三个的生命周期意味着任何人使用它都可以延长返回值的生命周期,而不是与B operator=(B)&&的生命周期相关联。

关于std::forward<T>(t) = std::forward<U>(u); 的唯一用途是它允许你相对统一地处理rvalue和左值代码。作为一个缺点,它可以让你在结果可能令人惊讶的情况下相对统一地对待它。

t
T&&是右值引用时,

可能无法编译而不是做一些令人惊讶的事情,例如“不修改t”。当T&&是左值参考时修改{{1}}同样错误。