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

How can I check if a move constructor is being generated implicitly?

本文关键字:构造函数 移动 何检查 检查 是否      更新时间:2023-10-16

>我有几个类,我想检查是否正在生成默认的移动构造函数。有没有办法检查这一点(无论是编译时断言,还是解析生成的对象文件,或者其他东西(?


励志例子:

class MyStruct : public ComplicatedBaseClass {
    std::vector<std::string> foo; // possibly huge
    ComplicatedSubObject bar;
};

如果任何基的任何成员或Complicated...Object类的任何成员无法移动,则MyStruct将不会生成其隐式移动构造函数,因此可能无法优化复制foo的工作,当可以完成移动时,即使foo是可移动的。


我希望避免:

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

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

  1. 显式使用 std::move - 如果没有可用的移动构造函数,这将调用复制构造函数。
  2. use 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的移动构造函数的符号,但不会得到复制构造函数,因为它似乎是完全隐式的。我假设编译器会生成一个普通的内联移动构造函数(如果可以(,如果它必须调用其他非平凡的移动构造函数,则生成一个非平凡的移动构造函数。不过,这仍然对我的追求没有帮助。

声明您希望存在于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;

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

正如 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. 禁用内联 ( -fno-inline (
    • 确保代码可以使用移动构造函数,或者(更好(
    • 临时添加对已编译代码中任意位置std::move(MyStruct)的调用,以满足 ODR 使用的要求
    • 确保MyStruct至少有一个父类或非静态成员(递归(,具有非平凡的移动构造函数(例如,std::string就足够了(,或者(更容易(
    • 临时将 std::string 成员添加到类中
  2. 通过nm -C ... | grep 'MyStruct.*&&'编译/链接并运行生成的对象文件

结果将暗示是否生成了移动构造函数。


正如问题本身所讨论的,这种方法似乎并不可靠,但在修复了使其不可靠的两个问题:内联和移动构造函数的琐碎性之后,它被证明是一种工作方法。

生成的移动构造函数是隐式还是显式默认不重要 - 默认值是否平凡是相关的:平凡移动(和复制(构造函数将只执行对象的字节复制。