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

Is the following move constructor code safe?

本文关键字:安全 代码 构造函数 移动      更新时间:2023-10-16

这是类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. 我们正在将rhs的相应成员移动到mbr1mbr2但是由于rhs已经从中移动(同样,它不能保证处于有效状态),为什么这应该工作?

这不是我的代码。我在一个网站上找到了它。此移动构造函数安全吗?如果是这样,如何?

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

假设base1base2是没有构造函数采用X/X&/X&&/const X&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))
{}

我相信,如果除了base1/base2/mbr1/mbr2之外没有其他基类或成员,编译器应该完全复制编译器为X(X&&) = default;隐式生成的内容。

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

这取决于继承层次结构。 但是很有可能这段代码很好。 这是一个完整的演示,显示它是安全的(对于这个特定的演示):

#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并没有真正移动。 这只是一个价值的演员,仅此而已。


因此,代码强制转换为右值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 Xn";}
};
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有一个采用右值的模板化构造函数。 如果此构造函数可以绑定到右值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&&)

世界又是美好的。

不,这将是可能的未定义行为的一个例子。它可能在很多时候有效,但不能保证。C++允许这样做,但您必须确保不会再次尝试以这种方式使用rhs。您正在尝试使用rhs三次,因为它的胆量被扯掉了,它的rvalue引用被传递给了一个可能从它移动的函数(使其处于未定义的状态)。