C++ 为什么不从临时对象(运算符+的结果)移动构造调用的CTR?
现在答案是:不要费心读这个问题,它有点长,可能不值得你花时间。我的代码中有bug,这就是为什么没有调用move构造函数的原因。查看答案以了解详细信息。请记住,RVO和NRVO命名的返回值优化可能会导致调用未按预期发生 我希望此行调用move-ctor,但会调用copy-ctor:C++ 为什么不从临时对象(运算符+的结果)移动构造调用的CTR?,c++,c++11,move-constructor,C++,C++11,Move Constructor,现在答案是:不要费心读这个问题,它有点长,可能不值得你花时间。我的代码中有bug,这就是为什么没有调用move构造函数的原因。查看答案以了解详细信息。请记住,RVO和NRVO命名的返回值优化可能会导致调用未按预期发生 我希望此行调用move-ctor,但会调用copy-ctor: Ding d3 = d1 + d2; Ding类有一个用户定义的move-ctor和on操作符+重载。我希望调用move-ctor的原因是操作符+返回一个临时对象,一个右值引用,因此可以进行移动优化 我在这里写的东西
Ding d3 = d1 + d2;
Ding类有一个用户定义的move-ctor和on操作符+重载。我希望调用move-ctor的原因是操作符+返回一个临时对象,一个右值引用,因此可以进行移动优化
我在这里写的东西可能是错误的,因为我是C++初学者。代码如下:
// Copied and modified code from here: https://stackoverflow.com/a/3109981
#include <iostream>
#include <cstring>
struct Ding {
char* data;
Ding(const char* p) {
std::cout << " ctor for: " << p << "\n";
size_t size = strlen(p) + 1;
data = new char[size];
memcpy(data, p, size);
}
~Ding() {
std::cout << " dtor for: " << data << "\n";
delete[] data;
}
Ding(const Ding& that) {
std::cout << " copy for: " << that.data << "\n";
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
Ding(Ding&& that) {
std::cout << " MOVE for: " << that.data << "\n";
data = that.data;
that.data = nullptr;
}
Ding& operator=(Ding that) {
std::cout << " assignment: " << that.data << "\n";
std::swap(data, that.data);
return *this;
}
Ding& operator+(const Ding that) const {
std::cout << " plus for: " << that.data << "\n";
size_t len_this = strlen(this->data);
size_t len_that = strlen(that.data);
char * tmp = new char[len_this + len_that + 1];
memcpy( tmp, this->data, len_this);
memcpy(&tmp[len_this], that.data, len_that + 1);
Ding * neu = new Ding(tmp);
return *neu;
}
};
void print(Ding d) {
std::cout << " (print): " << d.data << std::endl;
}
int main(void) {
std::cout << "putting a Ding on the stack\n";
Ding d1("jajaja");
std::cout << "calling print routine\n";
print(d1);
std::cout << "putting a second Ding on the stack\n";
Ding d2("nein");
// std::cout << "calling print routine\n";
// print(d2);
std::cout << "Putting a third Ding on the stack, init from + op ...\n";
std::cout << "... so expecting so see MOVE ctor used ...\n";
Ding d3 = d1 + d2;
// std::cout << "calling print routine\n";
// print(d3);
std::cout << "End of main, dtors being called ...\n";
}
两个二进制文件在程序结束时产生相同的输出和销毁顺序:
putting a Ding on the stack
ctor for: jajaja
calling print routine
copy for: jajaja
(print): jajaja
dtor for: jajaja
putting a second Ding on the stack
ctor for: nein
Putting a third Ding on the stack, init from + op ...
... so expecting so see MOVE ctor used ...
copy for: nein
plus for: nein
ctor for: jajajanein
dtor for: nein
copy for: jajajanein
End of main, dtors being called ...
dtor for: jajajanein
dtor for: nein
dtor for: jajaja
回想一下在这篇长文本之后的问题是什么:为什么move构造函数不调用dingd3=d1+d2
我知道还有其他一些问题,比如为什么不给搬家公司打电话,但我无法将他们的答案映射到这个案例中
使现代化
根据David Rodriguez的评论,我对计划进行了如下更改:
--- move-sem.cpp.orig 2012-03-17 17:00:56.901570900 +0100
+++ move-sem.cpp 2012-03-17 17:01:14.016549800 +0100
@@ -36,15 +36,14 @@
return *this;
}
- Ding& operator+(const Ding that) const {
+ Ding operator+(const Ding that) const {
std::cout << " plus for: " << that.data << "\n";
size_t len_this = strlen(this->data);
size_t len_that = strlen(that.data);
char * tmp = new char[len_this + len_that + 1];
memcpy( tmp, this->data, len_this);
memcpy(&tmp[len_this], that.data, len_that + 1);
- Ding * neu = new Ding(tmp);
- return *neu;
+ return tmp;
}
};
还有塔塔!现在,我看到了工作中的动作。。。但我认为现在还有另一个错误,新move-gcc.exe的输出不再列出dtor调用:
putting a Ding on the stack
ctor for: jajaja
calling print routine
copy for: jajaja
(print): jajaja
dtor for: jajaja
putting a second Ding on the stack
ctor for: nein
Putting a third Ding on the stack, init from + op ...
... so expecting so see MOVE ctor used ...
copy for: nein
plus for: nein
ctor for: jajajanein
MOVE for: jajajanein
dtor for:
第二次更新
我将坏运算符+替换为以下可能同样坏的代码:
Ding& operator+=(const Ding & rhs) {
std::cout << " op+= for: " << data << " and " << rhs.data << "\n";
size_t len_this = strlen(this->data);
size_t len_that = strlen(rhs.data);
char * buf = new char[len_this + len_that + 1];
memcpy( buf, this->data, len_this);
memcpy(&buf[len_this], rhs.data, len_that + 1);
delete[] data;
data = buf;
return *this;
}
Ding operator+(const Ding & rhs) const {
Ding temp(*this);
temp += rhs;
return temp;
}
我还从析构函数中删除了以下行,它阻止了程序异常终止:
std::cout << " dtor for: " << data << "\n";
当使用MSVC和g++-std=c++0x-fno elide构造函数编译时,现在调用move构造函数
这是错误的。您正在动态分配一个数据,然后强制它的一个副本。因为动态分配了数据,所以您正在泄漏它,它的生存期超出了return语句,编译器无法从中移动。请注意,您并没有返回一个临时文件
改为:
return Ding(tmp);
甚至:
return tmp;
由于采用const char*的构造函数不是显式的,编译器将使用它创建一个新的对象。在这两种情况下,临时语句的生存期都不会超过return语句,编译器将移动
这个答案假设您理解从返回对象到d3的副本已被省略,如果这是您预期的移动位置,那么编译器会做得更好:完全避免该操作
编辑为DeadMG已经编写了一个规范形式,但其中包含错误,我将跟进:
关于运算符重载有很多要说的,但我给出并遵循的一个常见建议是将operatorX=作为成员函数实现,这是一个应用于左侧的操作,然后根据前者将operator+作为自由函数实现。在C++11中,这将是:
class X {
X& operator+=( X const & ); // we do not modify the rhs internally
};
X operator+( X lhs, X const & rhs ) {
lhs += rhs; // reuse implementation
return lhs;
}
需要注意的一些事情:操作符+对于类型是对称的,因为它是一个自由函数。所有可能发生在右侧的隐式转换在lhs中也可用。在您的特定情况下,Ding可以从const char*隐式转换,这意味着通过使用free function操作符+可以编写:
Ding d( "A" );
const char* str = "B";
d + d; // no conversions
d + str; // conversion on the right hand side
str + d; // conversion on the left hand side
通过将operator+=定义为公共成员函数,您需要编写一个实现,并且该实现可以重用,因此您可以获得两个运算符,只需花费一行和三行额外的代码即可
按值指定参数并按值返回。这使编译器能够在参数为临时参数时删除参数副本。由于存在移动构造函数,因此也不会有任何内部副本,参数将被修改并移动到返回对象。这第二份不能删掉
不久前,我写了一些关于操作符重载的文章。。。它没有显式地处理优化移动,但是还有其他的帖子处理C++03版本的优化。从这一点到C++11特性,您应该能够填补空白
这是错误的。您正在动态分配一个数据,然后强制它的一个副本。因为动态分配了数据,所以您正在泄漏它,它的生存期超出了return语句,编译器无法从中移动。请注意,您并没有返回一个临时文件
改为:
return Ding(tmp);
甚至:
return tmp;
由于采用const char*的构造函数不是显式的,编译器将使用它创建一个新的对象。在这两种情况下,临时语句的生存期都不会超过return语句,编译器将移动
这个答案假设您理解从返回对象到d3的副本已被省略,如果这是您预期的移动位置,那么编译器会做得更好:完全避免该操作
编辑为DeadMG编写了一个规范形式,但它
包括错误,我将跟进:
关于运算符重载有很多要说的,但我给出并遵循的一个常见建议是将operatorX=作为成员函数实现,这是一个应用于左侧的操作,然后根据前者将operator+作为自由函数实现。在C++11中,这将是:
class X {
X& operator+=( X const & ); // we do not modify the rhs internally
};
X operator+( X lhs, X const & rhs ) {
lhs += rhs; // reuse implementation
return lhs;
}
需要注意的一些事情:操作符+对于类型是对称的,因为它是一个自由函数。所有可能发生在右侧的隐式转换在lhs中也可用。在您的特定情况下,Ding可以从const char*隐式转换,这意味着通过使用free function操作符+可以编写:
Ding d( "A" );
const char* str = "B";
d + d; // no conversions
d + str; // conversion on the right hand side
str + d; // conversion on the left hand side
通过将operator+=定义为公共成员函数,您需要编写一个实现,并且该实现可以重用,因此您可以获得两个运算符,只需花费一行和三行额外的代码即可
按值指定参数并按值返回。这使编译器能够在参数为临时参数时删除参数副本。由于存在移动构造函数,因此也不会有任何内部副本,参数将被修改并移动到返回对象。这第二份不能删掉
不久前,我写了一些关于操作符重载的文章。。。它没有显式地处理优化移动,但是还有其他的帖子处理C++03版本的优化。从这一点到C++11功能,您应该能够填补空白。没有什么可以移动的
你不能从d1或d2移动,因为那样会摧毁它们。并且您不能移动运算符+的返回值,因为它是一个引用。没有任何东西可以移动
你不能从d1或d2移动,因为那样会摧毁它们。并且不能移动运算符+的返回值,因为它是一个引用。您没有编写正确的加法运算符。标准形式是
Ding operator+(Ding other) const {
other += this;
return other;
// return std::move(other) if you're on MSVC
// which sometimes doesn't do this properly
}
Ding& operator+=(const Ding& other);
您的代码表现出许多问题,比如内存泄漏,这也是由于错误的运算符重载造成的
另外,不要忘记RVO和NRVO对预期输出的潜在影响。您没有编写正确的加法运算符。标准形式是
Ding operator+(Ding other) const {
other += this;
return other;
// return std::move(other) if you're on MSVC
// which sometimes doesn't do this properly
}
Ding& operator+=(const Ding& other);
您的代码表现出许多问题,比如内存泄漏,这也是由于错误的运算符重载造成的
此外,不要忘记RVO和NRVO对预期输出的潜在影响。对于operator+要返回一个临时值,需要返回一个叮当,而不是叮当&。我建议您开始一次学习一点。首先从调用约定开始,创建一个将记录操作的类,然后查看不同的函数签名是如何工作的。不应该有任何新的函数签名。如果需要字符串,请在清楚并理解它们之后使用std::string,阅读操作符重载和推荐的方法。最后,当你对此感到满意的时候,考虑一下管理你自己的资源,你甚至可以跳过这一部分,在大多数情况下,你不想管理资源。另外一件事是你不应该用不同的问题来编辑这个问题,因为答案不再有意义。现在,在编辑时,考虑调用已移动对象的析构函数时会发生什么。移动后,物体仍然可以被破坏。@DavidRodríguez dribeas,谢谢你的帮助和建议。-销毁过程中出现错误是因为在dtor中,当我正确地将数据成员设置为null ptr时,我尝试输出数据成员。所以修复方法是检查nullptr==数据?NULL:访问内存之前的数据。我认为该对象处于可以销毁的状态,只是析构函数坏了。@Lumi,我意识到了这一点。这就是发表评论的原因。如果你想要一个类似的跟踪输出,你可以打印cout,让operator+返回一个临时值,它需要返回一个Ding,而不是一个Ding&我建议你开始一次学习一位。首先从调用约定开始,创建一个将记录操作的类,然后查看不同的函数签名是如何工作的。不应该有任何新的函数签名。如果需要字符串,请在清楚并理解它们之后使用std::string,阅读操作符重载和推荐的方法。最后,当你对此感到满意的时候,考虑一下管理你自己的资源,你甚至可以跳过这一部分,在大多数情况下,你不想管理资源。另外一件事是你不应该用不同的问题来编辑这个问题,因为答案不再有意义。现在,在编辑时,考虑调用已移动对象的析构函数时会发生什么。移动后,物体仍然可以被破坏。@DavidRodríguez dribeas,谢谢你的帮助和建议。-销毁过程中出现错误是因为在dtor中,当我正确地将数据成员设置为null ptr时,我尝试输出数据成员。所以解决办法是检查n
ullptr==数据?NULL:访问内存之前的数据。我认为该对象处于可以销毁的状态,只是析构函数坏了。@Lumi,我意识到了这一点。这就是发表评论的原因。如果您想要类似的跟踪输出,您可以打印cout,谢谢。这对我来说是一个很好的教训。回到书本上,我想,在移动构造函数之前先学习运算符重载。添加了返回语句来完成运算符+实现。@DavidRodríguez dribeas:您会使用什么?T运算符+ta,const T&b{return a+=b;},也许?@JonPurdy:是的,这允许编译器在像a+b+c这样的情况下删除不必要的副本。在这个答案中,a+b的结果由一个引用绑定,然后手动复制。好的,哎呀,我尽可能快地将它从内存中删除了,它不是Bestzorz,但它是一个更好的起点,并确定了OP的核心问题:他的操作符重载被破坏。谢谢。这对我来说是一个很好的教训。回到书本上,我想,在移动构造函数之前先学习运算符重载。添加了返回语句来完成运算符+实现。@DavidRodríguez dribeas:您会使用什么?T运算符+ta,const T&b{return a+=b;},也许?@JonPurdy:是的,这允许编译器在像a+b+c这样的情况下删除不必要的副本。在这个答案中,a+b的结果由一个引用绑定,然后手动复制。好的,哎呀,我尽可能快地将它从内存中删除了,它不是Bestzorz,但它是一个更好的起点,并确定了OP的核心问题:他的操作符重载被破坏。谢谢。我在这里的另一篇博文中读到了这一点,所以我推测可能会有一些省略。当使用g++-std=c++0x-fno elide构造函数编译时,我确实看到我的move-ctor在工作,如上所述。但是现在一些输出丢失,程序异常终止,所以我的程序中有更多的错误。我假设他需要一个成员运算符,因为他已经编写了一个。诚然,我并不认为需要一个免费的运营商。@戴维,你的博客文章关于实现运营商+作为一个自由的功能对我来说是有意义的。好评论。我和其他人一样感谢你的帮助。谢谢。我在这里的另一篇博文中读到了这一点,所以我推测可能会有一些省略。当使用g++-std=c++0x-fno elide构造函数编译时,我确实看到我的move-ctor在工作,如上所述。但是现在一些输出丢失,程序异常终止,所以我的程序中有更多的错误。我假设他需要一个成员运算符,因为他已经编写了一个。诚然,我并不认为需要一个免费的运营商。@戴维,你的博客文章关于实现运营商+作为一个自由的功能对我来说是有意义的。好评论。我和其他人一样感谢你的帮助。