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