赋值运算符是否应该遵守赋值对象的重要性? [英] Should the assignment operator observe the assigned object's rvalueness?

查看:123
本文介绍了赋值运算符是否应该遵守赋值对象的重要性?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  int()= int // illegal:expression is not assignable

struct B {};
B& b = B()= B(); // compiles OK:产生一个左值! ...但是错了! (见下面)

对于最后一个语句,赋值运算符的结果实际上用于初始化非 - const 引用将在语句之后立即失效:引用不直接绑定到临时对象(它不能作为临时对象只能绑定到 const 或右值引用),但指向的生命期未延长的赋值结果。



问题是从赋值运算符返回的左值看起来好像它可以移动,虽然它实际上是指一个临时。如果任何东西使用赋值的结果来获取值,它将被复制而不是移动,虽然它是完全可行的移动。在这一点上,值得注意的是,该问题是用赋值运算符来描述的,因为这个运算符通常可用于值类型并返回一个左值引用。对任何返回对象引用的函数都存在相同的问题,即 * this



是重载赋值运算符(或返回对象引用的其他函数)以考虑对象的类型,例如:

  class G {
public:
//其他成员
G& operator =(G)& {/*...*/ return * this; }
G operator =(G)&& {/*...*/ return std :: move(* this); }
};

重载上述赋值运算符的可能性来自C ++ 11,对象无效,并且同时允许将分配的结果移动到临时的。这两个操作符的实现可能是相同的。虽然实现可能相当简单(基本上只是两个对象的 swap()),但它仍然意味着额外的工作提出了一个问题:



如果函数返回对对象的引用(例如,赋值运算符),则观察被赋值给的对象的重要性。



(由简单在注释中提及)不是重载赋值运算符,而是明确使用& 将其使用限制为左值:

  class GG {
public :
//其他成员
GG& operator =(GG)& {/*...*/ return * this; }
};
GG g;
g = GG(); // OK
GG()= GG() //错误


解决方案

IMHO, href =http://stackoverflow.com/users/1120273/dietmar-kuhl> DietmarKühl(为& 和<$ c $提供超载c>&&& ref-qualifiers)优于 Simple 的一个(提供它只适用于& )。
原来的想法是:

  class G {
public:
//其他成员
G& operator =(G)& {/*...*/ return * this; }
G operator =(G)&& {/*...*/ return std :: move(* this); }
};

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

  g = G()= G(); 

(如果需要),但是如果第二个重载被删除,那么这些行也无法编译: / p>

  const G& g1 = G()= G(); 
G&& g2 = G()= G();

我看不到他们不应该的原因(没有生命周期问题, =http://stackoverflow.com/users/1774667/yakk> Yakk 的帖子) 。



我只能看到一个 Simple 的情况建议是优选的:当 G 没有可访问的复制/移动构造函数。由于大多数类型的复制/移动赋值运算符都可以访问,因此也有一个可访问的复制/移动构造函数,这种情况是非常罕见的。



两个重载都以参数值并且有很好的理由,如果 G 有一个可访问的复制/移动构造函数。现在假设 G 没有。在这种情况下,运算符应该接受 const G& 的参数。



不幸的是第二个重载它不会返回一个引用(任何类型)到 * this ,因为 * this 绑定是一个右值,因此,它可能是一个临时的,其生存期即将到期。



在这种情况下,您应该删除第二个重载(根据Simple 的建议)否则类不会编译(除非第二个重载是一个不会被实例化的模板)。或者,我们可以保留第二个重载,并将其定义为 delete d。 (但是为什么要麻烦,因为& 单独的重载的存在已经足够了?)



c >&&& ? (我们再次假设 G 有一个可访问的复制/移动构造函数。)



作为 DietmarKühl已指出并 Yakk 已经探索过,这两个重载的代码应该非常类似,在这种情况下,最好实现&& & 。由于移动的执行是预期的不比一个副本差(并且因为RVO在返回 * this 时不适用),我们应该return std :: move(* this)。总而言之,一个可能的单行定义是:

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

如果只有 G 可以分配给另一个 G 或如果 G 有(非显式)转换构造函数。否则,您应该考虑给予 G a(模板)转发副本/移动赋值操作符,采用通用引用:

  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)



(注意,相关类型仅显示为返回类型。 +14它可以由编译器自动推导,因此,在最后两个代码片段中的 G 类型替换为 auto 。因此,宏可以成为一个类似于对象的宏,而不是类似函数的宏。)



另一种减少锅炉板代码量的方法是定义一个CRTP基类,它为&& 实现 operator = c $ c>:

 模板< typename Derived> 
struct assignment_for_rvalue {

template< typename T>
派生运算符=(T&& o)&& {
return std :: move(static_cast< Derived&>(* this)= std :: forward< T>(o));
}

};

锅炉板将成为继承和使用声明,如下所示:

  class G:public assignment_for_rvalue< G> {
public:
//其他成员,可能包括`&`
//的赋值运算符重载,但是使用不同类型和/或值类别的参数。
G& operator =(G)& {/*...*/ return * this; }

使用assignment_for_rvalue :: operator =;
};

回想一下,对于某些类型,相反使用 ASSIGNMENT_FOR_RVALUE ,继承 assignment_for_rvalue 可能会对类布局造成一些不必要的后果。


For class types it is possible to assign to temporary objects which is actually not allowed for built-in types. Further, the assignment operator generated by default even yields an lvalue:

int() = int();    // illegal: "expression is not assignable"

struct B {};
B& b = B() = B(); // compiles OK: yields an lvalue! ... but is wrong! (see below)

For the last statement the result of the assignment operator is actually used to initialize a non-const reference which will become stale immediately after the statement: the reference isn't bound to the temporary object directly (it can't as temporary objects can only be bound to a const or rvalue references) but to the result of the assignment whose life-time isn't extended.

Another problem is that the lvalue returned from the assignment operator doesn't look as if it can be moved although it actually refers to a temporary. If anything is using the result of the assignment to get hold of the value it will be copied rather than moved although it would be entirely viable to move. At this point it is worth noting that the problem is described in terms of the assignment operator because this operator is typically available for value types and returns an lvalue reference. The same problem exists for any function returning a reference to the objects, i.e., *this.

A potential fix is to overload the assignment operator (or other functions returning a reference to the object) to consider the kind of object, e.g.:

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

The possibility to overload the assignment operators as above has come with C++11 and would prevent the subtle object invalidation noted above and simultaneously allow moving the result of an assignment to a temporary. The implementation of the these two operators is probably identical. Although the implementation is likely to be rather simple (essentially just a swap() of the two objects) it still means extra work raising the question:

Should functions returning a reference to the object (e.g., the assignment operator) observe the rvalueness of the object being assigned to?

An alternatively (mentioned by Simple in a comment) is to not overload the assignment operator but to qualify it explicitly with a & to restrict its use to lvalues:

class GG {
public:
    // other members
    GG& operator=(GG) &  { /*...*/ return *this; }
};
GG g;
g = GG();    // OK
GG() = GG(); // ERROR

解决方案

IMHO, the original suggestion by Dietmar Kühl (providing overloads for & and && ref-qualifiers) is superior than Simple's one (providing it only for &). The original idea is:

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

and Simple has suggested to remove the second overload. Both solutions invalidate this line

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

(as wanted) but if the second overload is removed, then these lines also fail to compile:

const G& g1 = G() = G();
G&& g2 = G() = G();

and I see no reason why they shouldn't (there's no lifetime issue as explained in Yakk's post).

I can see only one situation where Simple's suggestion is preferable: when G doesn't have an accessible copy/move constructor. Since most types for which the copy/move assignment operator is accessible also have an accessible copy/move constructor, this situation is quite rare.

Both overloads take the argument by value and there are good reasons for that if G has an accessible copy/move constructor. Suppose for now that G does not have one. In this case the operators should take the argument by const G&.

Unfortunately the second overload (which, as it is, returns by value) should not return a reference (of any type) to *this because the expression to which *this binds to is an rvalue and thus, it's likely to be a temporary whose lifetime is about to expiry. (Recall that forbidding this from happening was one of the OP's motivation.)

In this case, you should remove the second overload (as per Simple's suggestion) otherwise the class doesn't compile (unless the second overload is a template that's never instantiated). Alternatively, we can keep the second overload and define it as deleted. (But why bother since the existence of the overload for & alone is already enough?)

A peripheral point.

What should be the definition of operator = for &&? (We assume again that G has an accessible copy/move constructor.)

As Dietmar Kühl has pointed out and Yakk has explored, the code of the both overloads should be very similar and, in this case, it's better to implement the one for && in terms of the one for &. Since the performance of a move is expected to be no worse than a copy (and since RVO doesn't apply when returning *this) we should return std::move(*this). In summary, a possible one-line definition is:

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

This is good enough if only G can be assigned to another G or if G has (non-explicit) converting constructors. Otherwise, you should instead consider giving G a (template) forwarding copy/move assignment operator taking an universal reference:

template <typename T>
G operator =(T&& o) && { return std::move(*this = std::forward<T>(o)); }

Although this is not a lot of boiler plate code it's still an annoyance if we have to do that for many classes. To decrease the amount of boiler plate code we can define a macro:

#define ASSIGNMENT_FOR_RVALUE(type) \
    template <typename T> \
    type operator =(T&& b) && { return std::move(*this = std::forward<T>(b)); }

Then inside G's definition one adds ASSIGNMENT_FOR_RVALUE(G).

(Notice that the relevant type appears only as the return type. In C++14 it can be automatically deduced by the compiler and thus, G and type in the last two code snippets can be replaced by auto. It follows that the macro can become an object-like macro instead of a function-like macro.)

Another way of reducing the amount of boiler plate code is defining a CRTP base class that implements operator = for &&:

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

};

The boiler plate becomes the inheritance and the using declaration as shown below:

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 =;
};

Recall that, for some types and contrarily to using ASSIGNMENT_FOR_RVALUE, inheriting from assignment_for_rvalue might have some unwanted consequences on the class layout.

这篇关于赋值运算符是否应该遵守赋值对象的重要性?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆