【问题标题】:How can I check if a move constructor is being generated implicitly?如何检查是否正在隐式生成移动构造函数?
【发布时间】:2016-02-29 14:40:12
【问题描述】:

我有几个类,我希望检查是否正在生成默认移动构造函数。有没有办法检查这一点(无论是编译时断言,还是解析生成的目标文件,或其他)?


励志例子:

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

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


我希望避免:

  1. 繁琐检查the conditions for implicit move ctor generation
  2. 显式和递归地默认所有受影响类的特殊成员函数、它们的基类和它们的成员 - 只是为了确保移动构造函数可用。

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

  1. 显式使用std::move — 如果没有可用的移动构造函数,这将调用复制构造函数。
  2. 使用std::is_move_constructible——当有一个接受const Type&amp;的复制构造函数时,这将成功,这是默认生成的(as long as the move constructor is not explicitly deleted, at least)。
  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&)

所以看来这种方法也行不通。


但是,如果目标类有一个具有显式移动构造函数的成员,则隐式生成的移动构造函数将对目标类可见。 IE。使用此代码:

#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 的移动构造函数的符号,但不是复制构造函数,因为它似乎是完全隐式的。我假设编译器会生成一个普通的内联移动构造函数,如果它必须调用其他非普通的移动构造函数,则会生成一个非普通的移动构造函数。不过,这仍然无法帮助我完成任务。

【问题讨论】:

  • 所以在上面的代码中,所有对copy ctor的使用都被省略了;它的存在被检查,但它没有被调用。 (如果您不认识,省略是 C++ 标准中的一个技术术语)。相比之下,实际调用了 move ctor。要强制调用复制 ctor,请写 template&lt;class T&gt; T const&amp; copy(T const&amp; t){return t;},然后写 MyStruct s2(copy(s1));。然后复制 ctor 可能会出现在您的转储中?
  • 重要的往往不是移动 ctor,而是移动操作是不抛出的。无法有效移动包含 1000 个原始字节数组的结构;定义移动 ctor 没有什么意义。复制 ctor 也可以完成这项工作。需要分配的结构通常会从移动中受益(因为您可以删除分配);在这种情况下,复制 ctor 可以抛出(分配失败),而移动 ctor 不会(因为它只是从传入的对象中窃取数据)。也许是那种方法?
  • @Yakk:我挑战你证明复制构造函数被省略 :-) 任何省略的基本前提是源对象和目标对象可以被视为一个和相同的。就像从函数返回本地对象或临时对象一样——如果我们首先在目标对象中构造该对象,则不会丢失任何内容。在这里,所有对象都是独立的,不能进行省略。
  • OTOH,您的第二条评论使我使用了具有显式复制和移动构造函数的 std::string。事实上,隐式生成的移动构造函数出现在MyStruct。我猜这与构造函数是否微不足道有关。请注意,任何优化级别根本不会导致构造函数符号 - 可能是由于内联。等等,-fno-inline?
  • 也许让你的复制构造函数抛出,然后使用is_nothrow_move_constructible

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


【解决方案1】:

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 怎么样?它不是签名的一部分,因此它需要在类定义中吗?我似乎记得这方面发生了一些变化(也许是针对析构函数的)?
  • @JohanLundberg 对,你在这两种情况下都声明它们为noexcept,如果默认版本不能为noexcept,编译器会报错。有一个变化使得不兼容的异常规范在它的第一个声明默认时不会导致硬错误(而是使函数被删除),但这与这里无关。
  • 在根据您的回答摆弄代码时,我意识到成员和基础不需要为 MyStruct 提供移动构造函数来隐式生成一个。他们只需要move_constructible——一个复制构造函数就足够了。检查MyStruct 是否定义了任何会阻止隐式声明的特殊成员是微不足道的,并且您的答案有效地回答了问题,所有成员和基础move_constructible
  • 不幸的是,这个答案只会让我更加困惑。 1)为什么move ctor的内部和外部默认行为不同? 2)当我默认它在里面,并且它会被隐式删除时,代码是有效的,但是复制ctor用于移动构造。好像内部的默认设置只强制 MyStructmove_constructible,但即便如此,还是使用了复制 ctor。这是有意的 C++11 设计吗?我做了an elaborate example to test this(只需切换第二个宏值)。
  • 我为我的第二个移动语义问题制作了a separate question,紧随其后的是这个问题。
【解决方案2】:

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

可以检查一个类型是否是平凡的或不能移动可构造的

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

限制;它还允许琐碎/无复制构造。

【讨论】:

  • 这个限制,结合非平凡的移动构造函数是问题所在。我对一个简单的移动构造函数没问题——这与一个简单的复制构造函数是一样的。但如果它不是微不足道的,我想知道是否真的会有一个。如果我定义自己的复制构造函数让它抛出,这样我就可以使用is_nothrow_move_constructible,那么根本不会有隐式生成的移动构造函数。
【解决方案3】:
  1. 禁用内联 (-fno-inline)
  2. 要么
    • 确保代码可以使用移动构造函数,或者(更好)
    • 在编译代码的任意位置临时添加对std::move(MyStruct)的调用以满足odr-used requirement
  3. 要么
    • 确保MyStruct 至少有一个父类或一个非静态成员(递归),并带有一个重要的移动构造函数(例如,std::string 就足够了),或者(更简单)
    • 暂时将 std::string 成员添加到您的类中
  4. 编译/链接并通过nm -C ... | grep 'MyStruct.*&amp;&amp;'运行生成的目标文件

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


正如问题本身所讨论的,这种方法似乎并不可靠,但在解决了使其不可靠的两个问题后:内联和triviality of the move constructor,结果证明它是一种有效的方法。

生成的移动构造函数是隐式默认还是显式默认无关紧要 - 默认值是否为 trivial or not 是相关的:一个普通的移动(和复制)构造函数将简单地执行对象的逐字节复制。

【讨论】:

    猜你喜欢
    • 2015-05-24
    • 2012-11-09
    • 2020-09-07
    • 2014-11-08
    • 2015-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多