如何检查移动构造函数是否被隐式生成?

18

我有几个类需要检查是否生成了默认移动构造函数。是否有一种方法可以检查这一点(无论是编译时断言,还是解析生成的目标文件,或其他方法)?


激励性例子:
class MyStruct : public ComplicatedBaseClass {
    std::vector<std::string> foo; // possibly huge
    ComplicatedSubObject bar;
};

如果任何基类成员或Complicated...Object类的任何成员无法移动,MyStruct将不会生成其隐式移动构造函数,并且在可以进行移动的情况下,可能无法优化复制foo的工作,即使foo是可移动的。请注意保留HTML标签。

我希望避免以下情况:

  1. 繁琐地检查隐式移动构造函数生成的条件,
  2. 显式且递归地默认所有受影响类、它们的基类和成员的特殊成员函数,只为了确保有一个移动构造函数可用。

我已经尝试过以下方法但它们不起作用:

  1. 显式使用 std::move —— 如果没有移动构造函数,则会调用复制构造函数。
  2. 使用 std::is_move_constructible —— 只要没有显式删除移动构造函数,就会成功,因为会默认生成一个接受 const Type& 的复制构造函数 (至少)。
  3. 使用 nm -C 检查是否存在移动构造函数 (见下文)。然而,还有一种替代方法可行 (请参见答案)。

我尝试查看像这样的简单类生成的符号: ```

```

#include <utility>

struct MyStruct {
    MyStruct(int x) : x(x) {}
    //MyStruct(const MyStruct& rhs) : x(rhs.x) {}
    //MyStruct(MyStruct&& rhs) : x(rhs.x) {}
    int x;
};

int main() {
    MyStruct s1(4);
    MyStruct s2(s1);
    MyStruct s3(std::move(s1));
    return s1.x + s2.x + s3.x; // Make sure nothing is optimized away
}

生成的符号看起来像这样:
$ CXXFLAGS="-std=gnu++11 -O0" make -B x; ./x; echo $?; nm -C x | grep MyStruct | cut -d' ' -f3,4,5
g++ -std=gnu++11 -O0    x.cc   -o x
12
.pdata$_ZN8MyStructC1Ei
.pdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.text$_ZN8MyStructC1Ei
.text$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.xdata$_ZN8MyStructC1Ei
.xdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
MyStruct::MyStruct(int)
std::remove_reference<MyStruct&>::type&&

当我显式默认复制和移动构造函数时(无符号),输出相同。

使用自己的复制和移动构造函数,输出如下:

$ vim x.cc; CXXFLAGS="-std=gnu++11 -O0" make -B x; ./x; echo $?; nm -C x | grep MyStruct | cut -d' ' -f3,4,5
g++ -std=gnu++11 -O0    x.cc   -o x
12
.pdata$_ZN8MyStructC1Ei
.pdata$_ZN8MyStructC1EOKS_
.pdata$_ZN8MyStructC1ERKS_
.pdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.text$_ZN8MyStructC1Ei
.text$_ZN8MyStructC1EOKS_
.text$_ZN8MyStructC1ERKS_
.text$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.xdata$_ZN8MyStructC1Ei
.xdata$_ZN8MyStructC1EOKS_
.xdata$_ZN8MyStructC1ERKS_
.xdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
MyStruct::MyStruct(int)
MyStruct::MyStruct(MyStruct&&)
MyStruct::MyStruct(MyStruct const&)
std::remove_reference<MyStruct&>::type&& std::move<MyStruct&>(MyStruct&)

所以看起来这种方法也不起作用。
然而,如果目标类具有显式移动构造函数的成员,则隐式生成的移动构造函数将对目标类可见。也就是说,使用以下代码时:
#include <utility>

struct Foobar {
    Foobar() = default;
    Foobar(const Foobar&) = default;
    Foobar(Foobar&&) {}
};

struct MyStruct {
    MyStruct(int x) : x(x) {}
    int x;
    Foobar f;
};
int main() {
    MyStruct s1(4);
    MyStruct s2(s1);
    MyStruct s3(std::move(s1));
    return s1.x + s2.x + s3.x; // Make sure nothing is optimized away
}

我将获取MyStruct的移动构造函数符号,但不是复制构造函数,因为它似乎完全是隐式的。我推测编译器如果可以生成一个平凡内联的移动构造函数,否则会生成一个非平凡的移动构造函数来调用其他非平凡的移动构造函数。然而这仍然不能帮助我完成我的任务。


所以在上面的代码中,所有对复制构造函数的使用都被省略了;它的存在被检查,但并没有被调用。(如果你不认识这个术语,省略是C++标准中的一个技术术语)。相比之下,移动构造函数实际上是被调用的。要强制调用复制构造函数,请编写template<class T> T const& copy(T const& t){return t;},然后MyStruct s2(copy(s1));。然后复制构造函数可能会出现在你的转储中。 - Yakk - Adam Nevraumont
重要的事情通常不是拥有一个移动构造函数,而是移动操作不会抛出异常。包含1000个原始字节数组的结构体无法高效地移动;定义移动构造函数没有多少意义。复制构造函数同样可以胜任这项工作。需要分配内存的结构体通常受益于移动(因为可以将分配的内存剥离);在这种情况下,复制构造函数可能会抛出异常(分配失败),而移动构造函数不会(因为它只是从传入的对象中窃取数据)。也许这种方法可行? - Yakk - Adam Nevraumont
@Yakk:我向你发起挑战,证明复制构造函数被省略了 :-) 任何省略的基本前提是源对象和目标对象可以被视为同一对象。就像从函数返回本地对象或临时对象一样——如果我们首先在目标对象中构造该对象,则不会丢失任何内容。在这里,所有对象都是分开的,因此无法执行省略。 - Irfy
另一方面,你的第二个评论让我使用了一个具有显式复制和移动构造函数的std::string。实际上,隐式生成的移动构造函数出现在MyStruct中。我猜这与构造函数是否为平凡的有关。请注意,任何优化级别都不会导致任何构造函数符号--可能是由于内联。等等,-fno-inline - Irfy
也许让你的复制构造函数抛出异常,然后使用 is_nothrow_move_constructible - M.M
显示剩余2条评论
3个回答

14

声明你想要存在于MyStruct中的特殊成员函数,但不要默认你想要检查的函数。假设你关心移动函数并且想要确保移动构造函数是noexcept的:

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&) = default;
    MyStruct(MyStruct&&) noexcept; // no = default; here
    MyStruct& operator=(const MyStruct&) = default;
    MyStruct& operator=(MyStruct&&); // or here
};

然后在类定义之外明确地将它们设置为默认值:

inline MyStruct::MyStruct(MyStruct&&) noexcept = default;
inline MyStruct& MyStruct::operator=(MyStruct&&) = default;

如果默认函数会被隐式定义为已删除,则会触发编译时错误。


不错。nothrow怎么样?它不是签名的一部分,因此需要在类定义中吗?我似乎记得在这个领域有些东西被改变了(也许是为了析构函数)? - Johan Lundberg
@JohanLundberg 对的,在这两种情况下你都声明它们为 noexcept,如果默认版本不能是 noexcept,编译器会报错。有一个变化使得不兼容的异常规范在第一次声明时不会引起严重错误(而是将函数删除),但这与此处无关。 - T.C.
在根据您的答案调整代码时,我意识到成员和基类不需要为MyStruct定义移动构造函数,就可以隐式生成一个。它们只需要是“可移动构造”——一个拷贝构造函数就足够了。检查是否 MyStruct 定义了任何特殊成员,这会阻止隐式声明,非常简单。您的回答有效地回答了这个问题:所有成员和基类都是“可移动构造”的吗? - Irfy
不幸的是,这个答案让我更加困惑了。1)为什么移动构造函数在内部和外部默认时行为不同?2)当我在内部默认它时,它会被隐式删除,代码是有效的,但复制构造函数用于移动构造。好像内部默认只强制MyStruct成为move_constructible,但即使如此,也使用复制构造函数。这是C++11的有意设计吗?我制作了一个详细的示例来测试(只需切换第二个宏值)。 - Irfy
我针对我的移动语义的第二个问题做了一个独立的问题,这个问题是从之前的问题引申而来的。 - Irfy
在移动赋值构造函数上故意省略 noexcept 是有意为之吗? - wrhall

5

正如Yakk所指出的,编译器是否生成通常并不相关。

可以 检查类型是否是平凡的或者是否具有nothrow移动构造函数

template< class T >
struct is_trivially_move_constructible;

template< class T >
struct is_nothrow_move_constructible;

http://en.cppreference.com/w/cpp/types/is_move_constructible

限制:它也允许平凡/不抛出复制构造函数。


1
这个限制与一个非平凡的移动构造函数结合在一起就是问题所在。我可以接受一个平凡的移动构造函数——这与平凡的复制构造函数是相同的。但如果它不是平凡的,我希望知道是否实际上会有一个。如果我定义自己的复制构造函数使其抛出异常,只是为了使用is_nothrow_move_constructible,那么将根本没有隐式生成的移动构造函数。 - Irfy

1
  1. 禁用内联 (-fno-inline)
  2. 要么
    • 确保代码可以使用移动构造函数,或者(更好的选择)
    • 在编译后的代码中暂时添加一个对 std::move(MyStruct) 的调用以满足 odr-used requirement
  3. 要么
    • 确保 MyStruct 至少有一个父类或非静态成员(递归地),具有非平凡的移动构造函数(例如,一个 std::string 就足够了),或者(更简单的方法)
    • 将一个 std::string 成员暂时添加到你的类中
  4. 编译/链接并通过 nm -C ... | grep 'MyStruct.*&&' 运行生成的目标文件

结果将表明是否生成了移动构造函数。


正如问题本身所讨论的那样,这种方法似乎不可靠,但在修复了两个导致其不可靠的问题后:内联和移动构造函数的琐碎性, 它被证明是一种可行的方法。
生成的移动构造函数是隐式的还是显式默认的并不重要 - 默认是否琐碎是相关的:一个琐碎的移动(和复制)构造函数只会执行对象的逐字节复制。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接