【问题标题】:Move Constructor vs Copy Elision. Which one gets called?移动构造函数与复制省略。哪个会被调用?
【发布时间】:2026-02-04 03:30:01
【问题描述】:

我这里有两段代码给你看。它们是两个类,每个类都提供一个移动构造函数和一个返回临时值的函数。

  • 在第一种情况下,返回一个临时的函数调用 Move 构造函数
  • 在第二种情况下,返回临时值的函数只是告诉编译器执行复制省略

我很困惑:在这两种情况下,我都定义了一个移动构造函数和一个返回临时值的随机成员函数。但是行为发生了变化,我的问题是为什么

请注意,在以下示例中,运算符


调用构造函数

template<typename T>
class GList
{
public:
    GList() : il{ nullptr } {}

    GList(const T& val) : il{ new Link<T>{ val,nullptr } }  {}

    GList(const GList<T>& copy) {}

    GList(GList<T>&& move)
    {
        std::cout << "[List] Move constructor called" << std::endl;

        // ... code ...
    }

    // HERE IS THE FUNCTION WHICH RETURNS A TEMPORARY!
    GList<T> Reverse()
    {
        GList<T> result;

        if (result.il == nullptr)
            return *this;

        ...
        ...
        ...

        return result;
    }
};

int main()
{

   GList<int> mylist(1);

   mylist.push_head(0);

   cout << mylist.Reverse();

   return 0;
}

输出是:

[List] 移动构造函数调用

0

1


复制省略

class Notemplate
{
   double d;
public:
   Notemplate(double val)
   {
      d = val;
   }

   Notemplate(Notemplate&& move)
   {
       cout << "Move Constructor" << endl;
   }

   Notemplate(const Notemplate& copy)
   {
       cout << "Copy" << endl;
   }

   Notemplate Redouble()
   {
       Notemplate example{ d*2 };
       return example;
   }
};

int main()
{
   Notemplate my{3.14};

   cout << my.Redouble();

   return 0;
}

输出是:

6.28


我期待在第二个示例中调用移动构造函数。 毕竟函数的逻辑都是一样的:返回一个临时的。

有人能解释一下为什么没有发生吗?

如何处理复制省略?

我希望我的代码尽可能地具有可移植性,我如何确定编译器会进行这些优化?

【问题讨论】:

  • 两个operator&lt;&lt;的签名是什么?
  • 朋友 std::ostream& 运算符

标签: c++ move-semantics move-constructor copy-elision


【解决方案1】:

another SO answer 的 cmets 中,OP 澄清了他在这里问的内容:

我听说即使有超过 1 个,也会发生复制省略 返回语句。我想知道何时禁止复制省略

所以我试图在这里解决这个问题:

复制/移动操作的省略(在 C++ 标准中称为 复制省略)在以下情况下是允许的:

  • 在具有类返回类型的函数中的 return 语句中,当 表达式 是具有自动存储持续时间的非易失性对象的名称(除了函数参数或处理程序的异常声明引入的变量)具有与函数返回类型相同的类型(忽略cv-qualification),可以通过将自动对象直接构造成省略复制/移动操作函数的返回值。

  • throw-expression 中,当操作数是范围不超出在最内层 try-block 的末尾(如果有),可以通过将自动对象直接构造到异常对象中来省略从操作数到异常对象的复制/移动操作。 p>

  • 当一个尚未绑定到引用的临时类对象将被复制/移动到具有相同类型的类对象(忽略 cv-qualification)时,可以通过构造将临时对象直接放入省略的复制/移动的目标中。

  • 当一个异常处理程序的exception-declaration声明了一个与异常对象相同类型的对象(除了cv-qualification),复制操作可以省略exception-declaration 作为异常对象的别名,如果程序的含义将保持不变,除了为异常声明声明的对象执行构造函数和析构函数。异常对象不能移动,因为它始终是左值。

在所有其他情况下都禁止复制省略。

函数中返回语句的数量与复制省略的合法性没有任何关系。然而,允许编译器执行复制省略,即使它是合法的,出于任何原因,包括返回语句的数量。

C++17 更新

现在有一些地方强制要求复制省略。如果 prvalue 可以直接绑定到按值函数参数、按值返回类型或命名局部变量,则在 C++17 中必须使用复制省略。这意味着编译器甚至无需检查复制或移动构造函数。合法的 C++17:

struct X
{
    X() = default;
    X(const X&) = delete;
    X& operator=(const X&) = delete;
};

X
foo(X)
{
    return X{};
}

int
main()
{
    X x = foo(X{});
}

【讨论】:

  • 谢谢教授 :) 有没有办法让我的代码尽可能便携?一种独立于编译器选择的方法
  • @gedamial:是的。不要将副作用放在与复制/移动对象无关的特殊成员(复制/移动构造函数)中(可能出于教育或调试目的除外)。这种副作用的例子是打印语句(如您的问题)或计数器(计算副本或移动的数量)。如果您确实有这样的副作用,请确保您的程序的正确性不依赖于这些副作用,以便在发生复制省略时,您的程序的正确性不会受到不利影响。
  • 非常感谢先生。你太棒了
【解决方案2】:

复制省略是当今每个现代编译器都提供的优化。

在 C++ 中返回巨大的类对象时,此技术适用... 但并非在所有情况下都适用!

在第一个示例中,编译器执行移动构造函数,因为我们在函数中有多个返回语句

【讨论】: