移动构造函数与复制省略。哪一个被调用?
Move Constructor vs Copy Elision. Which one gets called?
我这里有两段代码给你看。它们是两个 类,每个都提供一个移动构造函数和一个 return 临时函数。
- 在第一种情况下,函数 returning 临时调用 Move Constructor
- 在第二种情况下,return临时函数只是告诉编译器执行复制省略
我很困惑:在这两种情况下,我都定义了一个移动构造函数和一个临时的随机成员函数 return。但是行为发生了变化,我的问题是 为什么 .
请注意,在以下示例中,运算符<< 被重载以打印列表(在第一种情况下)和双精度数据成员(在第二种情况下)。
移动构造函数被调用
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] Move constructor called
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
我期待在第二个示例中调用移动构造函数。
毕竟,函数的逻辑是相同的:return 一个临时的。
有人能解释一下为什么没有发生吗?
如何处理复制省略?
我希望我的代码是最可移植的,我如何确定编译器进行了这些类型的优化?
复制省略是当今每个现代编译器都提供的优化。
在 C++ 中 returning 巨大的 class 对象时,此技术适用... 但并非在所有情况下!
在第一个示例中,编译器执行移动构造函数,因为我们在函数中有多个 return 语句。
在 another SO answer 的评论中,OP 澄清了他在这里的问题:
I heard that copy elision CAN occur even when there are more than 1
return statements. I'd like to know when a copy elision is forbidden
所以我试图在这里解决这个问题:
省略 copy/move 操作(在 C++ 标准中称为 复制省略)在以下情况下是允许的:
在具有 class return 类型的函数中的 return
语句中,当 表达式 是具有自动存储持续时间的非易失性对象的名称(函数参数或由处理程序的 exception-declaration 引入的变量除外)具有相同类型(忽略 cv-qualification)作为函数return类型,copy/move操作可以通过构造自动对象直接进入函数的return值来省略copy/move操作。
在 throw-expression 中,当操作数是非易失性自动对象的名称时(函数或 catch 子句参数除外)其范围不超出最内层封闭 try-block(如果有)的末尾,则可以省略从操作数到异常对象的 copy/move 操作将自动对象直接构造成异常对象。
当未绑定到引用的临时 class 对象将 copied/moved 到具有相同类型的 class 对象(忽略 cv- qualification), copy/move 操作可以通过直接构造临时对象到被省略的 copy/move.
的目标中来省略
当异常处理器的exception-declaration声明了一个与异常对象相同类型(除了cv-qualification)的对象时,副本如果程序的含义除了执行由异常声明。异常对象不能移动,因为它始终是左值。
在所有其他情况下禁止复制省略。
函数中return语句的数量与复制省略的合法性没有任何关系。然而,编译器被允许 不 执行复制省略,即使它是合法的,出于任何原因,包括 return 语句的数量。
C++17 更新
现在有一些地方强制复制省略。如果纯右值可以直接绑定到按值函数参数,或按值 return 类型,或绑定到命名局部变量,则复制省略在 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{});
}
我这里有两段代码给你看。它们是两个 类,每个都提供一个移动构造函数和一个 return 临时函数。
- 在第一种情况下,函数 returning 临时调用 Move Constructor
- 在第二种情况下,return临时函数只是告诉编译器执行复制省略
我很困惑:在这两种情况下,我都定义了一个移动构造函数和一个临时的随机成员函数 return。但是行为发生了变化,我的问题是 为什么 .
请注意,在以下示例中,运算符<< 被重载以打印列表(在第一种情况下)和双精度数据成员(在第二种情况下)。
移动构造函数被调用
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] Move constructor called
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
我期待在第二个示例中调用移动构造函数。 毕竟,函数的逻辑是相同的:return 一个临时的。
有人能解释一下为什么没有发生吗?
如何处理复制省略?
我希望我的代码是最可移植的,我如何确定编译器进行了这些类型的优化?
复制省略是当今每个现代编译器都提供的优化。
在 C++ 中 returning 巨大的 class 对象时,此技术适用... 但并非在所有情况下!
在第一个示例中,编译器执行移动构造函数,因为我们在函数中有多个 return 语句。
在 another SO answer 的评论中,OP 澄清了他在这里的问题:
I heard that copy elision CAN occur even when there are more than 1 return statements. I'd like to know when a copy elision is forbidden
所以我试图在这里解决这个问题:
省略 copy/move 操作(在 C++ 标准中称为 复制省略)在以下情况下是允许的:
在具有 class return 类型的函数中的
return
语句中,当 表达式 是具有自动存储持续时间的非易失性对象的名称(函数参数或由处理程序的 exception-declaration 引入的变量除外)具有相同类型(忽略 cv-qualification)作为函数return类型,copy/move操作可以通过构造自动对象直接进入函数的return值来省略copy/move操作。在 throw-expression 中,当操作数是非易失性自动对象的名称时(函数或 catch 子句参数除外)其范围不超出最内层封闭 try-block(如果有)的末尾,则可以省略从操作数到异常对象的 copy/move 操作将自动对象直接构造成异常对象。
当未绑定到引用的临时 class 对象将 copied/moved 到具有相同类型的 class 对象(忽略 cv- qualification), copy/move 操作可以通过直接构造临时对象到被省略的 copy/move.
的目标中来省略
当异常处理器的exception-declaration声明了一个与异常对象相同类型(除了cv-qualification)的对象时,副本如果程序的含义除了执行由异常声明。异常对象不能移动,因为它始终是左值。
在所有其他情况下禁止复制省略。
函数中return语句的数量与复制省略的合法性没有任何关系。然而,编译器被允许 不 执行复制省略,即使它是合法的,出于任何原因,包括 return 语句的数量。
C++17 更新
现在有一些地方强制复制省略。如果纯右值可以直接绑定到按值函数参数,或按值 return 类型,或绑定到命名局部变量,则复制省略在 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{});
}