2012-05-20 4 views
7

Недавно я был на лекции с Бьярне Стуступом, он говорил о C++ 11 и почему это имело смысл.Именно поэтому конструктор перемещения в C++ 11 имеет смысл?

Одним из его примеров новой удивительности была новость «& &» символ для перемещения конструкторов.

Затем я хочу домой и начал думать: «Когда мне когда-нибудь понадобится такая штука?».

Мой первый пример был ниже код:

class Number { 
private: 
    int value; 
public: 
    Number(const int value) : value(value){ 
     cout << "Build Constructor on " << value << endl; 
    } 
    Number(const Number& orig) : value(orig.value){ 
     cout << "Copy Constructor on " << value << endl;   
    } 
    virtual ~Number(){} 

    int toInt() const{ 
     return value; 
    } 


    friend const Number operator+(const Number& n0, const Number& n1); 
}; 

const Number operator+(const Number& n0, const Number& n1){ 
    return Number(n0.value + n1.value); 
} 

int main(int argc, char** argv) { 

    const Number n3 = (Number(2) + Number(1)); 
    cout << n3.toInt() << endl; 
    return 0; 
} 

Этот код делает именно то, что конструктор шаг должен решить. Переменная n3 построена из ссылки на значение, возвращаемое оператором «+».

Кроме этого есть выход из работы коды:

Build Constructor on 1 
Build Constructor on 2 
Build Constructor on 3 
3 

RUN SUCCESSFUL 

Что выход показывает, что конструктор копирования никогда не вызывается - и это с оптимизациями выключены. Мне тяжело перекручивать руку кода достаточно, чтобы заставить запустить консоль. обертывание результата в std :: pair сделало трюк, но это заставило меня задуматься.

Является ли аргумент move-constructors в операторной арифметике фактически неудачным аргументом?

Почему is'nt мой конструктор копирования называется и почему он называется в:

using namespace std; 

class Number { 
private: 
    int value; 
public: 
    Number(const int value) : value(value){ 
     cout << "Build Constructor on " << value << endl; 
    } 
    Number(const Number& orig) : value(orig.value){ 
     cout << "Copy Constructor on " << value << endl;   
    } 
    virtual ~Number(){} 

    int toInt() const{ 
     return value; 
    } 


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
}; 

const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){ 
    return make_pair(Number(n0.value + n1.value), n0); 
} 

int main(int argc, char** argv) { 

    const Number n3 = (Number(2) + Number(1)).first; 
    cout << n3.toInt() << endl; 
    return 0; 
} 

С выходом:

Build Constructor on 1 
Build Constructor on 2 
Copy Constructor on 2 
Build Constructor on 3 
Copy Constructor on 3 
Copy Constructor on 2 
Copy Constructor on 3 
Copy Constructor on 2 
Copy Constructor on 3 
3 

RUN SUCCESSFUL 

Я хотел бы знать, что логика и почему оператор пары в основном завинчивает производительность?

обновление:

Я сделал еще одну модификацию и обнаружил, что если я заменил make_pair с фактическим шаблонного конструктора пары pair<const Number, const Number> это уменьшило количество раз конструктор копирования был уволен:

class Number { 
private: 
    int value; 
public: 
    Number(const int value) : value(value){ 
     cout << "Build Constructor on " << value << endl; 
    } 
    Number(const Number& orig) : value(orig.value){ 
     cout << "Copy Constructor on " << value << endl;   
    } 
    virtual ~Number(){} 

    int toInt() const{ 
     return value; 
    } 


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
}; 



const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){ 
    return std::pair<const Number, const Number>(Number(n0.value + n1.value), n0); 
} 

int main(int argc, char** argv) { 

    const Number n3 = (Number(2) + Number(1)).first; 
    cout << n3.toInt() << endl; 
    return 0; 
} 

:

Build Constructor on 1 
Build Constructor on 2 
Build Constructor on 3 
Copy Constructor on 3 
Copy Constructor on 2 
Copy Constructor on 3 
3 

RUN SUCCESSFUL 

Таким образом, это будет груша с использованием make_pair вредно?

+0

Я только что проверил первый пример, и я получил бесконечный рекурсивный цикл в 'operator +': is добавляет 'int' и' Number', 'int' по умолчанию преобразуется в' Number' и то все начинается снова. – rodrigo

+2

Конструктор перемещения имеет смысл только для классов, управляющих внешними данными. Содержимое самих данных объекта * всегда * копируется, так или иначе. –

+0

@rodrigo: В примере была опечатка, я исправил ее. –

ответ

13

Рассмотрим простой C++ код:

class StringHolder 
{ 
    std::string member; 
public: 

    StringHolder(const std::string &newMember) : member(newMember) {} 
}; 

std::string value = "I am a string that will probably be heap-allocated."; 
StringHolder hold(value); 

После выполнения второй строки, сколько копий строки существуют? Ответ два: один хранится в value, а один хранится в hold. Это нормально ... иногда. Часто бывают случаи, когда вы хотите дать кому-то копию строки, сохраняя ее для себя. Но есть моменты, когда вы не тоже хотите это сделать.Например:

StringHolder hold("I am a string that will probably be heap-allocated."); 

Это создаст std::string временный характер, который затем будет передан StringHolder «s конструктор. Конструктор будет копировать-построить свой член. После завершения конструктора временная информация будет уничтожена. В какой-то момент у нас было две копии строки, без всякой причины.

Не было смысла иметь две копии строки. То, что мы хотели сделать, было переместить в StringHolder, так что есть только одна копия строки.

Вот где ход строительства приходит.

std::string в основном только оберткой указатель выделенного массива символов, а размер, содержащий длину этого массива (и емкость, но фигу, что сейчас). Если у вас есть std::string, и вы хотите, чтобы переместил в другую, новая строка должна требовать права собственности на выделенный массив символов, а старая строка должна отказаться от права собственности. В C++ 03, вы можете сделать это с помощью swap операции:

std::string oldStr = "I am a string that will probably be heap-allocated."; 
std::string newStr; 
std::swap(newStr, oldStr); 

Это перемещает содержимое oldStr в newStr без выделения памяти.

Синтаксис перемещения C++ 11 обеспечивает две важные функции, которых нет в std::swap.

Во-первых, перемещение может происходить неявно (но только, когда это безопасно). Вы должны явно позвонить swap, если вы хотите обменять; перемещение может происходить путем написания естественного кода. К примеру, взять нашу StringHolder до и сделать одно изменение:

class StringHolder 
{ 
    std::string member; 
public: 

    StringHolder(std::string newMember) : member(std::move(newMember)) {} 
}; 

StringHolder hold("I am a string that will probably be heap-allocated."); 

Сколько копий этой строки когда-либо создан? Ответ ... только один: строительство временного. Поскольку он является временным, C++ 11 достаточно умен, чтобы знать, что он может перемещать-строить что-либо, инициализируемое им. Поэтому он перемещает - создает параметр значения конструктора StringHolder (или, скорее всего, полностью скрывает конструкцию). Это перемещает сохраненную память из временной в newMember. Поэтому копирования не происходит.

После этого мы явно вызываем конструктор перемещения при построении member. Это снова перемещает выделенную память от newMember до member.

Только выделить строку раз. Это может быть большой экономией производительности.

Теперь, как это относится к конструкторам ваших собственных типов? Ну, рассмотрим этот код:

class StringHolder 
{ 
    std::string member; 
public: 

    StringHolder(std::string newMember) : member(std::move(newMember)) {} 

    StringHolder(const StringHolder &old) : member(old.member) {} 
    StringHolder(StringHolder &&old) : member(std::move(old.member)) {} 
}; 

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated."); 
StringHolder newHold(oldHold); 

На этот раз у нас теперь есть класс с конструктором копирования и перемещения. Сколько копий строки мы получаем?

Два. Конечно, это два.У нас есть oldHold и newHold, каждая с копией строки.

Но, если бы мы сделали это:

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated."); 
StringHolder newHold(std::move(oldHold)); 

Тогда снова только когда будет один копия строки, лежащие вокруг.

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


Почему is'nt мой конструктор копирования называется

Ваш конструктор копирования не был назван, потому что он был опущены. Это делает оптимизацию возвращаемых значений. Отключение оптимизации не поможет, потому что большинство компиляторов все равно откажутся. Нет причин не делать это, когда это возможно.

Для возвращаемых значений функции движение важно в случаях, когда elision равен не возможно.

+0

Ненавижу говорить, я это знаю, но знаю. Моя самая большая проблема заключалась в том, что причина, по которой Stoustrup выдвигалась вперед для конструктора перемещения, заключалась в том, что он хотел написать код типа «Matrix A = (B * C) + Y;», где матрица могла иметь большое распределение mem, которое должны быть скопированы arround. То, что я обнаружил в своих экспериментах, заключалось в том, что этого копирования никогда не было. Теперь вы говорите его из-за «элиции», и это имеет смысл для меня. Но это sorta делает аргумент «его для делания арифметики» недействительным. –

+1

@ Мартин: Во-первых, Страуструп не Бог; его слово не является законом, потому что так оно и есть. Для меня это конкретное использование движения мало чем отличается от того, зачем оно нам нужно. Во-вторых, разрешение разрешено спецификацией, но не * обязательным *. Поэтому для компилятора совершенно законно * не * копировать/перемещать. Таким образом, поддержка движений по-прежнему хорошая. В-третьих, исключение происходит только при создании объекта. Если вы этого не сделаете, если вы повторно используете некоторую уже существующую переменную, то elision не может вам помочь. –

+0

Я согласен, что он не Бог. И я вижу, что операции перемещения/копирования имеют смысл при работе с такими вещами, как STL. Я не могу не задаться вопросом, не похоже ли на то, что 'std :: make_pair' производит двойные конструкции и разрушения, как версия без - должна быть достаточной причиной для ее удаления из STL, но это совершенно другая история.Я думаю, что вы получите очки, но мы дадим et в день ;-) –

2

Это может помочь понять значение хода семантики, если вы перегружать operator+ с этим:

Number operator+(Number&& n0, Number&& n1){ 
    n0.value += n1.value; 
    return std::move(n0); 
} 

Это имеет два важных изменение:

  • он возвращает неконстантное значение
  • принимает аргументы rvalue и модифицирует один из них вместо создания нового объекта

Это позволяет ваш пример, чтобы избежать один из «Build Конструктора» называет

Build Constructor on 1 
Build Constructor on 2 
Copy Constructor on 3 
3 

Теперь код создает два новых объекта и копии одного, nstead создания трех новых, так что не является большим преимуществом до сих пор. Но если добавить конструктор перемещения, который может быть использован вместо копии:

Number(Number&& orig) : value(orig.value){ 
    cout << "Move Constructor on " << value << endl; 
} 

Build Constructor on 1 
Build Constructor on 2 
Move Constructor on 3 
3 

Если класс выделяет память в «Build» и конструкторах «Копировать» вы сократили общее число отчислений из трех в вашем Исходный код на два (при условии, конструктор перемещения ничего не выделяет, но занимает право собственности на память, которая принадлежит объект, который он перемещается из.)

Теперь, если вы измените расчет на:

Number n3 = Number(2) + Number(1) + Number(0); 

И сравнить исходный код с версия с поддержкой перемещения вы должны увидеть количество «распределений», уменьшенное с пяти до трех. Чем больше времени задействовано, тем больше польза от изменения и перемещения из временных, а не для создания новых объектов. Преимущество заключается не только в том, чтобы избежать копирования, но и в том, чтобы не создавать новые ресурсы для новых объектов, вместо этого взяв на себя ответственность за ресурсы из существующих объектов.

+0

Я вижу, куда вы идете, но ... ваш пример - небольшой бизар, вы определяете оператор +, который разрушает состояние одного из его оперантов. Я готов представить себе, что такие вещи есть где-то ... Я не знаю финансов? но такой код никогда не будет использоваться? 'Номер один (1); Число три = один + один + один, 'равно, неудачно? (если у объекта есть внешние ресурсы?) –

+3

В этом весь смысл rvalues, они привязаны к временным сторонам, которые уходят, поэтому безопасно их разрушать, потому что никто не может сказать, мутировало ли неназванное временное время, которое должно было появиться. Ваш новый пример не будет терпеть неудачу, потому что 'one' является lvalue, который не может привязываться к ссылке rvalue, так что выражение вызывает ваш исходный' operator + (const Number &, const Number &) 'overload, а не мою новую перегрузку rvalue. Может быть, вы действительно должны пойти и прочитать что-то о ссылках на rvalue. –

+1

Если вы все еще не уверены, мой «причудливый» пример безопасен и полезен, полагайте, что в C++ 11 'std :: string' имеет точно такую ​​же' operator + 'перегрузку, принимающую rvalues ​​и не утечку памяти, если вы пишете' s + s + s' ... люди, разрабатывающие язык и библиотеку, могут просто знать, что они делают;) –

Смежные вопросы