UPDATE внизу
q1: Как бы вы реализовали правило из пяти вариантов для класса, который управляет довольно тяжелыми ресурсами, но вы хотите, чтобы его передавали по значению, потому что это значительно упрощает и украшает его использование? Или нужны еще не все пять элементов правила?
На практике я начинаю что-то с 3D-изображением, где изображение обычно составляет 128 * 128 * 128 удваивается. Быть способным писать такие вещи, чтобы сделать математику намного проще:
Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2: Используя комбинацию семантики copy elision/RVO/move, компилятор должен уметь это с минимальным копированием, не?
Я попытался выяснить, как это сделать, поэтому я начал с основ; предположим, что объект реализует традиционный способ реализации копирования и назначения:
class AnObject
{
public:
AnObject( size_t n = 0 ) :
n( n ),
a( new int[ n ] )
{}
AnObject( const AnObject& rh ) :
n( rh.n ),
a( new int[ rh.n ] )
{
std::copy( rh.a, rh.a + n, a );
}
AnObject& operator = ( AnObject rh )
{
swap( *this, rh );
return *this;
}
friend void swap( AnObject& first, AnObject& second )
{
std::swap( first.n, second.n );
std::swap( first.a, second.a );
}
~AnObject()
{
delete [] a;
}
private:
size_t n;
int* a;
};
Теперь введите rvalues и переместите семантику. Насколько я могу судить, это будет рабочая реализация:
AnObject( AnObject&& rh ) :
n( rh.n ),
a( rh.a )
{
rh.n = 0;
rh.a = nullptr;
}
AnObject& operator = ( AnObject&& rh )
{
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}
Однако компилятор (VС++ 2010 SP1) не слишком доволен этим, и компиляторы обычно правильны:
AnObject make()
{
return AnObject();
}
int main()
{
AnObject a;
a = make(); //error C2593: 'operator =' is ambiguous
}
q3: Как это решить? Возвращаясь к AnObject & operator = (const AnObject & rh), конечно, исправляет его, но разве мы не теряем довольно важную возможность оптимизации?
Кроме того, он ясно, что код для конструктора и назначения перемещения полон дублирования. Поэтому на данный момент мы забываем о двусмысленности и пытаемся решить эту проблему с помощью копирования и свопинга, но теперь для rvalues. Как объяснено here, нам даже не нужен пользовательский своп, но вместо этого std:: swap выполняет всю работу, что звучит очень многообещающе. Поэтому я написал следующее, надеясь, что std:: swap скопирует временную конструкцию с помощью конструктора move, а затем поменяет его на * this:
AnObject& operator = ( AnObject&& rh )
{
std::swap( *this, rh );
return *this;
}
Но это не сработает и вместо этого приводит к переполнению стека из-за бесконечной рекурсии, поскольку std:: swap снова вызывает наш оператор = (AnObject && rh). q4: Может ли кто-нибудь представить пример того, что имеется в примере в этом примере?
Мы можем решить это, предоставив вторую функцию подкачки:
AnObject( AnObject&& rh )
{
swap( *this, std::move( rh ) );
}
AnObject& operator = ( AnObject&& rh )
{
swap( *this, std::move( rh ) );
return *this;
}
friend void swap( AnObject& first, AnObject&& second )
{
first.n = second.n;
first.a = second.a;
second.n = 0;
second.a = nullptr;
}
Теперь там почти в два раза больше кода суммы, однако часть его движения платит за счёт довольно дешевого перемещения; но, с другой стороны, обычное назначение больше не может извлечь выгоду из копирования. На данный момент я действительно запутался, и, не видя больше, что правильно и что не так, я надеюсь получить здесь немного информации.
UPDATE Итак, кажется, что есть два лагеря:
- одно высказывание, чтобы пропустить оператор присваивания перемещения и продолжать делать то, что нам учил С++ 03, т.е. написать один оператор присваивания, который передает аргумент по значению.
- другой, говорящий, чтобы реализовать оператор назначения перемещения (в конце концов, это С++ 11 сейчас) и оператор присваивания копии принимает свой аргумент по ссылке.
(хорошо, и там 3-й лагерь говорит мне использовать вектор, но этот вид вне сферы действия для этого гипотетического класса. Хорошо в реальной жизни я бы использовал вектор, и были бы и другие члены, но так как move constructor/assign не генерируются автоматически (пока?) вопрос все равно будет сохраняться)
К сожалению, я не могу протестировать обе реализации в реальном мире, так как этот проект только начался, и способ фактического потока данных пока неизвестен. Поэтому я просто реализовал оба из них, добавил счетчики для выделения и т.д. И выполнил пару итераций ок. этот код, где T является одной из реализаций:
template< class T >
T make() { return T( narraySize ); }
template< class T >
void assign( T& r ) { r = make< T >(); }
template< class T >
void Test()
{
T a;
T b;
for( size_t i = 0 ; i < numIter ; ++i )
{
assign( a );
assign( b );
T d( a );
T e( b );
T f( make< T >() );
T g( make< T >() + make< T >() );
}
}
Либо этот код недостаточно хорош, чтобы проверить, что мне нужно, либо компилятор слишком умный: не имеет значения, что я использую для arraySize и numIter, результаты для обоих лагерей в значительной степени идентичны: одинаковое число распределений, очень незначительные изменения в сроках, но не воспроизводимые существенные различия.
Поэтому, если кто-то не может указать на лучший способ проверить это (учитывая, что фактическое использование scnearios пока не известно), я должен сделать вывод, что это не имеет значения и, следовательно, остается на вкус разработчика, В этом случае я бы выбрал № 2.