Почему перемещение временного объекта не требуется для строительства из временного объекта (результат оператора +)? - PullRequest
1 голос
/ 17 марта 2012

Теперь, когда на него дан ответ: не пытайтесь читать этот вопрос, он немного длинен и, вероятно, не стоит вашего времени. В моем коде были ошибки, и по этой причине конструктор перемещения не был вызван. Проверьте ответы для деталей. Помните, что RVO и NRVO (оптимизация именованных возвращаемых значений) могут учитывать вызовы, которые не происходят так, как вы ожидаете.


Я ожидаю, что ctor перемещения будет вызываться для этой строки, но вместо этого вызывается копия ctor:

Ding d3 = d1 + d2;

Класс Ding имеет пользовательский ctor перемещения и оператор + перегрузка. Причина, по которой я ожидаю вызова 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";
}

Вызовы компилятора (на Win7) для VC2010 Express и MinGW (GCC 4.6) следующие:

cl /nologo /W4 /EHsc /MD move-sem.cpp
g++ -std=c++0x move-sem.cpp -o move-gcc.exe

Оба двоичных файла выдают одинаковый результат (без порядка уничтожения в конце программы):

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

Вспомните, какой вопрос был после длинного текста: почему конструктор перемещения не вызвал Ding d3 = d1 + d2;?

Я знаю, что есть другие вопросы относительно того, почему не вызывают вызовы ctors, но я не могу сопоставить их ответы с этим делом.

Обновление

Я изменил программу в соответствии с комментариями Дэвида Родригеса:

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

Затем я перекомпилировал программу, используя вызовы компилятора, упомянутые выше, и получил вывод, где была удалена одна копия (copy for: jajajanein). Затем я попробовал следующую строку:

g++ -std=c++0x -fno-elide-constructors move-sem.cpp -o move-gcc.exe

И тата! Теперь я вижу движение ctor на работе! ... Но я думаю, что есть еще одна ошибка, вывод этой новой 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:

Второе обновление

Я заменил плохой operator+ следующим (возможно, одинаково плохим) кодом:

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-constructors.

Ответы [ 3 ]

2 голосов
/ 17 марта 2012
    Ding * neu = new Ding(tmp);
    return *neu;

Это неправильно.Вы динамически выделяете Ding и затем принудительно копируете его.Поскольку Ding выделяется динамически, вы просачиваетесь , его время жизни выходит за пределы оператора return, и компилятор не может переместить из него.Обратите внимание, что вы не возвращаете временное значение.

Измените его на:

    return Ding(tmp);

Или даже:

    return tmp;

Поскольку ваш конструктор принимаетconst char* не является явным , компилятор будет использовать его для создания нового Ding объекта.В обоих случаях время жизни временного объекта не выходит за пределы оператора 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;
}

Некоторые вещи, на которые следует обратить внимание: operator+ симметричен относительно типов, поскольку является свободной функцией.Все неявные преобразования, которые могут произойти с правой стороны, также доступны в lhs.В вашем конкретном случае, Ding неявно конвертируется из const char*, что означает, что, имея свободную функцию operator+, вы можете написать:

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 вы сможете заполнить пробелы.

1 голос
/ 17 марта 2012

Вы не написали правильный оператор сложения.Каноническая форма:

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на ожидаемом выходе.

1 голос
/ 17 марта 2012

Нечего перемещать.

Вы не можете перейти от d1 или d2, потому что это уничтожит их. И вы не можете переместить возвращаемое значение operator+, потому что это ссылка.

...