C++ 为什么不从临时对象(运算符+的结果)移动构造调用的CTR?

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的原因是操作符+返回一个临时对象,一个右值引用,因此可以进行移动优化 我在这里写的东西

现在答案是:不要费心读这个问题,它有点长,可能不值得你花时间。我的代码中有bug,这就是为什么没有调用move构造函数的原因。查看答案以了解详细信息。请记住,RVO和NRVO命名的返回值优化可能会导致调用未按预期发生

我希望此行调用move-ctor,但会调用copy-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在工作,如上所述。但是现在一些输出丢失,程序异常终止,所以我的程序中有更多的错误。我假设他需要一个成员运算符,因为他已经编写了一个。诚然,我并不认为需要一个免费的运营商。@戴维,你的博客文章关于实现运营商+作为一个自由的功能对我来说是有意义的。好评论。我和其他人一样感谢你的帮助。