模板是否应该为不同类型的参数制作非 Rvalue 引用构造函数/赋值

Should templates make non-Rvalue-reference constructors/assigns for move only parameters of different type?

本文关键字:数制 Rvalue 引用 赋值 构造函数 参数 是否 同类型      更新时间:2023-10-16

假设我有一个仅移动类型。 我们停止默认提供的构造函数存在,但 Rvalue 引用引入了一种新的"风格",我们可以用于签名的移动版本:

class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;
public:
    CantCopyMe (CantCopyMe && other) {
        /* ... */
    }
    CantCopyMe & operator= (CantCopyMe && other) {
        /* ... */
    }
};

我最近认为你总是应该通过 Rvalue 引用传递可移动类型。 现在看起来只有非常特殊的情况才需要这样做......像这两个。 如果你把它们放在任何地方,事情似乎在大多数情况下都有效,但我只是发现了编译器没有运行代码转移所有权部分的一种情况。

(这种情况就像传递变量中保存的唯一指针,std::move传递到采用unique_ptr<foo> &&参数的东西......但是注意到调用站点上的变量没有被清空。 将参数更改为unique_ptr<foo>修复了它,并且它已正确为空,从而防止了双重删除。 :-/我没有隔离为什么这个在其他地方似乎有效时很糟糕,但吸烟枪是第一次工作,但随后的电话没有。

我相信这是有充分理由的,你们中的许多人都可以突出地总结一下。 与此同时,我开始像一个优秀的货物崇拜程序员一样四处走动,删除 &&s。

但是,如果您正在编写一个模板化类,它看起来像这样呢?

template <class FooType>
class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;
public:
    template<class OtherFooType>
    CantCopyMe (CantCopyMe<OtherFooType> && other) {
        /* ... */
    }
    template<class OtherFooType>
    CantCopyMe & operator= (CantCopyMe<OtherFooType> && other) {
        /* ... */
    }
};

这是出于某种原因的不良做法吗,当 OtherFooType 和 FooType 不一样时,您应该分开休息......那么它只是按值传递?

template <class FooType>
class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;
public:
    CantCopyMe (CantCopyMe && other) {
        /* ... */
    }
    CantCopyMe & operator= (CantCopyMe && other) {
        /* ... */
    }
    template<class OtherFooType>
    CantCopyMe (CantCopyMe<OtherFooType> other) {
        /* ... */
    }
    template<class OtherFooType>
    CantCopyMe & operator= (CantCopyMe<OtherFooType> other) {
        /* ... */
    }
};

我认为有一个可能意想不到的原因有一个简单的答案:

复制/移动构造函数或赋值运算符从来都不是模板(专用化)。 例如 [class.copy]/2

X 的非模板构造函数是复制构造函数,如果它的第一个参数是 X&const X&volatile X&const volatile X& 的类型,并且没有其他参数,或者所有其他参数都有默认参数。

另外,脚注122说:

由于模板赋值运算符或采用 rvalue 引用参数的赋值运算符从来都不是复制赋值运算符,因此此类赋值运算符的存在不会禁止显示复制赋值运算符的隐式声明。此类赋值运算符与其他赋值运算符(包括复制赋值运算符)一起参与重载解析,如果选中,将用于分配对象。

例:

#include <iostream>
#include <utility>
template<class T>
struct X
{
    X() {}
    template<class U>
    X(X<U>&&)
    {
        std::cout << "template "move" ctorn";
    }
    template<class U>
    X& operator= (X<U>&&)
    {
        std::cout << "template "move" assignment-opn";
        return *this;
    }
};
int main()
{
    X<int> x;                     // no output
    X<int> y(x);                  // no output
    y = std::move(x);             // no output
    X<double> z( std::move(x) );  // output
    y = std::move(z);             // output
}

在此示例中,使用隐式声明的移动构造函数和移动赋值运算符。


因此,如果不声明非模板移动 ctor 和移动赋值运算符,则可能会隐式声明它们。它们不会隐式声明,例如,如果您有用户声明的 dtor,则用于移动分配操作;有关详细信息,请参阅 [class.copy]/11 和 [class.copy]/20。

示例:

向上面的示例添加 dtor:

#include <iostream>
#include <utility>
template<class T>
struct X
{
    X() {}
    ~X() {}
    template<class U>
    X(X<U>&&)
    {
        std::cout << "template "move" ctorn";
    }
    template<class U>
    X& operator= (X<U>&&)
    {
        std::cout << "template "move" assignment-opn";
        return *this;
    }
};
int main()
{
    X<int> x;                     // no output
    X<int> y(x);                  // no output
    y = std::move(x);             // output
    X<double> z( std::move(x) );  // output
    y = std::move(z);             // output
}

在这里,第一个移动分配y = std::move(x);调用分配运算符模板的专用化,因为没有隐式声明的移动分配运算符。