5

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

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

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

请注意,在以下示例中,运算符<< 被重载以打印列表(在第一种情况下)和双数据成员(在第二种情况下)。


移动构造函数被调用

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;
}

输出是:

[列表] 移动构造函数调用

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


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

有人会解释我为什么没有发生吗?

如何处理复制省略?

我希望我的代码是最便携的,我如何确定编译器的这些优化?

4

2 回答 2

11

另一个 SO answer的评论中,OP 澄清了他在这里提出的问题:

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

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

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

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

  • throw-expression中,当操作数是非易失性自动对象(函数或 catch 子句参数除外)的名称时,其范围不超出最内层封闭try 块的末尾(如果有),可以通过将自动对象直接构造到异常对象中来省略从操作数到异常对象的复制/移动操作。

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

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

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

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

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{});
}
于 2016-03-18T22:08:15.770 回答
1

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

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

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

于 2016-02-20T12:39:48.040 回答