#include <iostream> #include <vector> #ifdef __GXX_EXPERIMENTAL_CXX0X__ #define NOEXCEPT noexcept #define STDMOVE( x ) std::move( ( x ) ) #else #define NOEXCEPT #define STDMOVE( x ) ( x ) #endif #ifdef DEBUG #define VSIZE 1 #define DPRINT( x ) std::cout << ( x ) << std::endl; #else #define VSIZE 10000 #define DPRINT( x ) #endif namespace { const size_t d_size( 1024 ); const size_t v_size( VSIZE ); } class A { public: A() : data( new int[ d_size ] ) { DPRINT( "Constructor" ) } ~A() NOEXCEPT { DPRINT( "Destructor" ) delete[] data; } A( const A & a ) : data( new int[ d_size ] ) { DPRINT( "Const reference" ) /* May not assign 'a->data' to 'this->data' as 'a' may go out of * scope and be destructed, therefore 'a.data' will be deleted */ /* this->data = a.data; */ /* May not assign 'a.data' as it is const reference */ /* a.data = NULL; */ /* May only copy data! */ for ( int i( 0 ); i < d_size; ++i ) { this->data[ i ] = a.data[ i ]; } } #ifdef __GXX_EXPERIMENTAL_CXX0X__ A( A && a ) noexcept { DPRINT( "Rvalue reference" ) /* May assign 'a.data' to 'this->data' because 'a' is * rvalue reference */ this->data = a.data; /* 'a.data' must be uninitialized to avoid double deletion! */ a.data = nullptr; } #endif private: int * data; }; int main( void ) { std::vector< A > v( v_size ); for ( int i( 0 ); i < v_size; ++i ) { A a; v.push_back( STDMOVE( a ) ); } }В самом начале находятся определения макросов NOEXCEPT и STDMOVE: они нужны для того, чтобы программу можно было скомпилировать как с поддержкой расширений C++11, так и без нее. В случае компиляции без поддержки C++11 эти макросы не расширяются, а при поддержке - превращаются соответственно в noexcept и std::move. Далее идут определения макросов VSIZE и DPRINT: если собирать программу с флагом -DDEBUG, то DPRINT позволит выводить важные сообщения на экран терминала, а VSIZE, который мы будем использовать для определения размера вектора v, будет равен 1 - это позволит не захламлять экран большим числом сообщений, но при этом даст возможность убедиться, что вызываются правильные конструкторы.
Внутри класса A определены последовательно: конструктор A(), деструктор ~A(), копирующий конструктор, конструктор с семантикой переноса (завернутый в макрос __GXX_EXPERIMENTAL_CXX0X__) и член data. В функции main() создается вектор v, дальше в цикле локальный объект a типа A добавляется в конец вектора v.
Проверим, что все работает правильно. Для этого сначала соберем нашу программу без поддержки C++11 и семантики переноса, но с флагом -DDEBUG:
g++ -g -O2 -DDEBUG -o test main.cppЗапустим собранную программу:
$ ./test Constructor Const reference Destructor Constructor Const reference Const reference Destructor Destructor Destructor DestructorОтлично, строки Const reference свидетельствуют о том, что были вызваны конструкторы копирования. Теперь соберем программу с поддержкой C++11:
g++ -g -O2 -std=c++11 -DDEBUG -o test main.cppЗапускаем:
$ ./test Constructor Constructor Rvalue reference Rvalue reference Destructor Destructor Destructor DestructorТеперь были вызваны конструкторы с семантикой переноса! Кстати, обратим внимание на очевидные различия при инициализации вектора без поддержки и с поддержкой C++11. Теперь сравним производительность: компилируем оба варианта без флага -DDEBUG (теперь в вектор будет записано 10000 элементов типа A) и смотрим, насколько конструктор с семантикой переноса быстрее конструктора копирования.
- g++ -g -O2 -o test main.cpp
$ time ./test real 0m0.070s user 0m0.036s sys 0m0.030s
- g++ -g -O2 -std=c++11 -o test main.cpp
$ time ./test real 0m0.047s user 0m0.007s sys 0m0.038s
Теперь о некоторых второстепенных деталях. Во-первых, если в push_back() передавать не именованный объект класса A, а созданный на месте объект A(), то можно обойтись без std::move(): стандарт не позволяет рассматривать именованные объекты как ссылки на rvalue, в то время как неименованные таковыми являются. Задачей std::move() как раз и является превращение именованного объекта в ссылку на rvalue. Во-вторых, если вы уберете слово noexcept из деструктора или конструктора с семантикой переноса, то вы получите вызов конструктора копирования в дебрях алгоритма копирования элементов в std::vector. Попробуйте убрать это слово хотя бы из одной из этих функций, скомпилируйте программу с флагом -DDEBUG, и вы увидите всё сами.
Очень подробно вопросы, связанные с семантикой переноса, рассматриваются в этой статье. В частности, там показано, почему так важны std::move() и noexcept.