以下移动构造函数代码是否安全?

时间:2014-01-25 01:46:23

标签: c++ c++11 move-semantics

这是类X的移动构造函数:

X::X(X&& rhs)
    : base1(std::move(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{ }

这些是我警惕的事情:

  1. 我们正在从rhs 移动两次,并且rhs无法保证处于有效状态。对于base2
  2. 的初始化,这不是未定义的行为吗?
  3. 我们正在将rhs的相应成员移至mbr1mbr2,但由于rhs已经移出(并且再次确保不会出现有效状态)为什么要这样做?
  4. 这不是我的代码。我在一个网站上找到了它。这个移动构造函数安全吗?如果是这样,怎么样?

3 个答案:

答案 0 :(得分:16)

这取决于继承层次结构。但是这个代码很好的机会非常好。这是一个完整的演示,表明它是安全的(对于这个特定的演示):

#include <iostream>

struct base1
{
    base1() = default;
    base1(base1&&) {std::cout << "base1(base1&&)\n";}
};

struct base2
{
    base2() = default;
    base2(base2&&) {std::cout << "base2(base2&&)\n";}
};

struct br1
{
    br1() = default;
    br1(br1&&) {std::cout << "br1(br1&&)\n";}
};

struct br2
{
    br2() = default;
    br2(br2&&) {std::cout << "br2(br2&&)\n";}
};

struct X
    : public base1
    , public base2
{
    br1 mbr1;
    br2 mbr2;

public:
    X() = default;
    X(X&& rhs)
    : base1(std::move(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
    { }
};

int
main()
{
    X x1;
    X x2 = std::move(x1);
}

应输出:

base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)

在这里您可以看到每个基地和每个成员只移动一次。


请记住:std::move并没有真正动起来。它只是对右值的一个演员,仅此而已。


因此,代码转换为rvalue X,然后将其传递给基类。假设基类看起来像我上面概述的那样,那么对于右值base1base2进行隐式转换,它将移动构造这两个单独的基数。

此外,


请记住:移动的对象处于有效但未指定的状态。


只要base1base2的移动构造函数不会进入派生类并更改mbr1mbr2,那么这些成员就是仍处于已知状态,准备离开。没问题。

现在我确实提到可能会出现问题。这是如何:

#include <iostream>

struct base1
{
    base1() = default;
    base1(base1&& b)
         {std::cout << "base1(base1&&)\n";}
    template <class T>
    base1(T&& t)
        {std::cout << "move from X\n";}
};

struct base2
{
    base2() = default;
    base2(base2&& b)
        {std::cout << "base2(base2&&)\n";}
};

struct br1
{
    br1() = default;
    br1(br1&&) {std::cout << "br1(br1&&)\n";}
};

struct br2
{
    br2() = default;
    br2(br2&&) {std::cout << "br2(br2&&)\n";}
};

struct X
    : public base1
    , public base2
{
    br1 mbr1;
    br2 mbr2;

public:
    X() = default;
    X(X&& rhs)
    : base1(std::move(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
    { }
};

int
main()
{
    X x1;
    X x2 = std::move(x1);
}

在这个例子中,base1有一个带有rvalue-something的模板化构造函数。如果此构造函数可以绑定到右值X,并且此构造函数将从右值X然后移动,则会出现问题:

move from X
base2(base2&&)
br1(br1&&)
br2(br2&&)

解决此问题的方法(相对较少,但并非极为罕见),是forward<base1>(rhs)而不是move(rhs)

    X(X&& rhs)
    : base1(std::forward<base1>(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
    { }

现在base1看到右值base1而不是右值X,它将绑定到base1移动构造函数(假设它存在),所以你再次得到:

base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)

再一次对世界都很好。

答案 1 :(得分:15)

这大约是隐式移动构造函数通常如何工作的方式:每个基础和成员子对象都是从rhs的相应子对象移动构造的。

假设base1base2X的基础,没有构造函数使用X / X& / X&& / {{ 1}},它写得很安全。传递给基类初始值设定项时,const X&将隐式转换为std::move(rhs)base1&&}。

编辑:当我在一个完全匹配base2&&的基类中有一个模板构造函数时,这个假设实际上已经咬了我几次。明确地执行转换会更安全(虽然非常迂腐):

X&&

甚至只是:

X::X(X&& rhs)
    : base1(std::move(static_cast<base1&>(rhs)))
    , base2(std::move(static_cast<base2&>(rhs)))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{}
如果没有其他基类或成员而不是X::X(X&& rhs) : base1(static_cast<base1&&>(rhs)) , base2(static_cast<base2&&>(rhs)) , mbr1(std::move(rhs.mbr1)) , mbr2(std::move(rhs.mbr2)) {} / X(X&&) = default; / base1 / {我认为应该完全复制编译器为base2隐式生成的内容{1}}。

再次编辑:C ++11§12.8/ 15描述了隐式成员复制/移动构造函数的确切结构。

答案 2 :(得分:2)

不,这将是可能未定义行为的示例。它可能会在很多时候起作用,但不能保证。 C ++允许它,但你必须确保不再尝试以这种方式再次使用rhs。你试图在之后使用rhs三次它的内容已经被删除 它的rvalue引用被传递给一个可能会从它移动的函数(离开它)处于未定义的状态)。