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

 大工 发布于 2023-02-07 05:42

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

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的建议)否则该类不编译(除非第二个重载是一个从未实例化的模板).或者,我们可以保留第二个重载并将其定义为deleted.(但是,为什么&单独存在过载已经足够了?)

外围点.

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

作为迪特马尔·库尔指出和Yakk探索了,两个重载的代码应该是非常相似的,在这种情况下,最好是实施一个&&在一个方面&.由于移动的表现预计不会比副本差(并且由于RVO在返回时不适用*this)我们应该返回std::move(*this).总之,可能的一行定义是:

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

如果只能G将其分配给另一个GG具有(非显式)转换构造函数,那么这就足够了.否则,您应该考虑G使用(模板)转发复制/移动赋值运算符来获取通用引用:

template 
G 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它可以通过编译器自动推断,因此,Gtype在最后两个代码段可以被替换auto.由此可见,宏可以成为一个对象类似宏而不是像函数一样的宏.)

减少的锅炉板代码量的另一种方式是定义一个实现一个CRTP基类operator =&&:

template 
struct 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可能会对类布局产生一些不良后果.

1 个回答
  • 恕我直言,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的建议)否则该类不编译(除非第二个重载是一个从未实例化的模板).或者,我们可以保留第二个重载并将其定义为deleted.(但是,为什么&单独存在过载已经足够了?)

    外围点.

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

    作为迪特马尔·库尔指出和Yakk探索了,两个重载的代码应该是非常相似的,在这种情况下,最好是实施一个&&在一个方面&.由于移动的表现预计不会比副本差(并且由于RVO在返回时不适用*this)我们应该返回std::move(*this).总之,可能的一行定义是:

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

    如果只能G将其分配给另一个GG具有(非显式)转换构造函数,那么这就足够了.否则,您应该考虑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.由此可见,宏可以成为一个对象类似宏而不是像函数一样的宏.)

    减少的锅炉板代码量的另一种方式是定义一个实现一个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可能会对类布局产生一些不良后果.

    2023-02-07 06:23 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有