何时调用移动构造函数?

36

我对于移动构造函数和复制构造函数何时被调用感到困惑。我读了以下资源:Move constructor is not getting called in C++0xC++11中的移动语义和右值引用MSDN文章

这些资源要么太复杂了(我只想要一个简单的例子),要么只展示如何编写移动构造函数,而不是如何调用它。我写了一个简单的问题来更加具体:

const class noConstruct{}NoConstruct;
class a
{
private:
    int *Array;
public:
    a();
    a(noConstruct);
    a(const a&);
    a& operator=(const a&);
    a(a&&);
    a& operator=(a&&);
    ~a();
};

a::a()
{
    Array=new int[5]{1,2,3,4,5};
}
a::a(noConstruct Parameter)
{
    Array=nullptr;
}
a::a(const a& Old): Array(Old.Array)
{

}
a& a::operator=(const a&Old)
{
    delete[] Array;
    Array=new int[5];
    for (int i=0;i!=5;i++)
    {
        Array[i]=Old.Array[i];
    }
    return *this;
}
a::a(a&&Old)
{
    Array=Old.Array;
    Old.Array=nullptr;
}
a& a::operator=(a&&Old)
{
    Array=Old.Array;
    Old.Array=nullptr;
    return *this;
}
a::~a()
{
    delete[] Array;
}

int main()
{
    a A(NoConstruct),B(NoConstruct),C;
    A=C;
    B=C;
}

目前A、B和C的指针值不同。我想让A有一个新的指针,让B具有C的旧指针,让C具有一个空指针。

有点离题,但如果有人能建议我一个可以详细了解这些新功能的文档,我将不胜感激,并且可能不需要再问很多问题。


1
在一个部分相关的问题上,你可能想要检查一下拷贝并交换惯用语(copy and swap idiom) https://dev59.com/73A75IYBdhLWcg3weY1f 以用于你的赋值运算符。 - undu
4个回答

48
移动构造函数是在以下情况下被调用:
  • 当使用std::move(something)作为对象初始化器时
  • 当使用std::forward<T>(something)作为对象初始化器时,且T不是左值引用类型(在模板编程中实现"完美转发"很有用)
  • 当对象初始化器是临时对象且编译器无法完全消除复制/移动操作时
  • 当以值的形式返回函数局部类对象且编译器无法完全消除复制/移动操作时
  • 当抛出函数局部类对象并且编译器无法完全消除复制/移动操作时
注意,这不是一个完整的列表。如果参数具有类类型(而非引用类型),则“对象初始化器”可以是函数参数。
a RetByValue() {
    a obj;
    return obj; // Might call move ctor, or no ctor.
}

void TakeByValue(a);

int main() {
    a a1;
    a a2 = a1; // copy ctor
    a a3 = std::move(a1); // move ctor

    TakeByValue(std::move(a2)); // Might call move ctor, or no ctor.

    a a4 = RetByValue(); // Might call move ctor, or no ctor.

    a1 = RetByValue(); // Calls move assignment, a::operator=(a&&)
}

1
a4 = RetByValue(); // 可能调用移动构造函数,也可能不调用任何构造函数。//<--是否调用取决于什么?这与编译器优化有关吗?谢谢 - artm
3
是的,这取决于编译器。实际上,C++17在规则上进行了一些改变,以保证某些额外的情况下减少构造函数和析构函数的调用次数。 - aschepler
是的,这取决于编译器,这就是为什么有些人会发疯! :-)在我的情况下,当我尝试删除默认移动构造函数时(我的意思是:“class_name(class_name &&)= delete;”),我遇到了一个错误。错误是:“error: use of deleted function 'class_name::class_name(class_name&&)"。代码错误行是“class_name test = class_name("test");”。然后我尝试定义“我的自定义移动构造函数”,它似乎只在使用“std::move”时才被调用。这也取决于编译器吗?提前致谢! P.s.:对于长评论,我表示歉意... - bitfox
1
@bitfox 编译器不能选择代码是否正确,只能在某些情况下选择如果代码有效会发生什么。当 class_name 具有显式删除的移动构造函数时,语句 class_name test = class_name("test"); 在 C++11 和 C++14 中是不合法的,但在 C++17 中是合法的,因为没有涉及移动或复制构造函数。我认为您无法检测出 std::move(e) 表达式和其他右值表达式之间的区别。 - aschepler
@aschepler 感谢您的回复。您关于C++版本和“不良形式”的建议,促使我对一些问题进行整理。这个网址http://howardhinnant.github.io/classdecl.html和https://dev59.com/nFoU5IYBdhLWcg3wzZE1让我清楚了很多东西。 - bitfox

5

首先,你的复制构造函数有问题。被复制的对象和复制后的对象都会指向同一个 Array,并且在它们超出作用域时都会尝试 delete[] 它,导致未定义行为。要修复它,需要复制数组。

a::a(const a& Old): Array(new int[5])
{
  for( size_t i = 0; i < 5; ++i ) {
    Array[i] = Old.Array[i];
  }
}

现在,移动赋值并不像你想的那样执行,因为两个赋值语句都是从左值赋值,而不是使用右值。要执行移动操作,必须从右值移动,或者必须在上下文中将左值视为右值(例如函数的返回语句)。

为了获得所需效果,请使用std::move创建一个右值引用。

A=C;              // A will now contain a copy of C
B=std::move(C);   // Calls the move assignment operator

B=std::move(C); // 调用移动赋值运算符。随后,C可能不再是原来的对象了。 - RainingChain

1

上面的回答并没有提供一个当调用移动构造函数时自然的例子。我发现了一种不使用std::move(也不通过-fno-elide-constructors来抑制拷贝省略)调用移动构造函数的方法:

a foo(a a0) {
    return a0; // move ctor is called 
}
    
a a1 = foo(a());

1
目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

0

请记住可能会发生拷贝省略。如果通过向编译器传递-fno-elide-constructors标志禁用它,则可能会执行您的构造函数。

您可以在此处阅读有关此内容的信息:https://www.geeksforgeeks.org/copy-elision-in-c/


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