Перемещение конструкторов и статических массивов
Я изучал возможности Move Constructors на С++, и мне было интересно, какие способы использовать эту функцию в пример, такой как ниже. Рассмотрим этот код:
template<unsigned int N>
class Foo {
public:
Foo() {
for (int i = 0; i < N; ++i) _nums[i] = 0;
}
Foo(const Foo<N>& other) {
for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
}
Foo(Foo<N>&& other) {
// ??? How can we take advantage of move constructors here?
}
// ... other methods and members
virtual ~Foo() { /* no action required */ }
private:
int _nums[N];
};
Foo<5> bar() {
Foo<5> result;
// Do stuff with 'result'
return result;
}
int main() {
Foo<5> foo(bar());
// ...
return 0;
}
В этом выше примере, если мы трассируем программу (с MSVС++ 2011), мы видим, что при построении foo
вызывается Foo<N>::Foo(Foo<N>&&)
, что является желаемым поведением. Однако, если бы у нас не было Foo<N>::Foo(Foo<N>&&)
, вместо него было бы вызвано Foo<N>::Foo(const Foo<N>&)
, что сделало бы операцию избыточной копии.
Мой вопрос, как отмечено в коде, с этим конкретным примером, который использует статически распределенный простой массив, есть ли способ использовать конструктор перемещения, чтобы избежать этой избыточной копии?
Ответы
Ответ 1
Во-первых, есть общий совет, в котором говорится, что вы не должны писать какой-либо конструктор копирования/перемещения, оператор присваивания или деструктор вообще, если вы можете ему помочь, и, скорее, составить свой класс высококачественных компонентов, которые, в свою очередь, предоставляйте их, позволяя функциям, созданным по умолчанию, делать правильную вещь. (Обратное следствие состоит в том, что если вам нужно написать какой-либо из них, вам, вероятно, придется написать все из них.)
Итак, вопрос сводится к тому, "какой компонент компонента одной ответственности может использовать семантику перемещения?" Общий ответ: все, что управляет ресурсом. Дело в том, что конструктор/получатель перемещения просто переустанавливает ресурс на новый объект и делает недействительным старый, что позволяет избежать (предполагаемого дорогого или невозможного) нового распределения и глубокого копирования ресурса.
Главный пример - это все, что управляет динамической памятью, где операция перемещения просто копирует указатель и устанавливает старый указатель объекта на ноль (так что деструктор старого объекта ничего не делает). Здесь наивный пример:
class MySpace
{
void * addr;
std::size_t len;
public:
explicit MySpace(std::size_t n) : addr(::operator new(n)), len(n) { }
~MySpace() { ::operator delete(addr); }
MySpace(const MySpace & rhs) : addr(::operator new(rhs.len)), len(rhs.len)
{ /* copy memory */ }
MySpace(MySpace && rhs) : addr(rhs.addr), len(rhs.len)
{ rhs.len = 0; rhs.addr = 0; }
// ditto for assignment
};
Ключ состоит в том, что любой конструктор копирования/перемещения будет выполнять полное копирование переменных-членов; только когда эти переменные сами обрабатывают или указатели на ресурсы, которые вы можете избежать копирования ресурса, из-за соглашения о том, что перемещенный объект больше не считается действительным и что вы можете его украсть. Если нечего воровать, тогда нет никакой пользы в движении.
Ответ 2
В этом случае это не полезно, потому что int
не имеет конструкторов move.
Однако было бы полезно, если бы это были строки, например:
template<unsigned int N>
class Foo {
public:
// [snip]
Foo(Foo<N>&& other) {
// move each element from other._nums to _nums
std::move(std::begin(other._nums), std::end(other._nums), &_nums[0]);
}
// [snip]
private:
std::string _nums[N];
};
Теперь вы избегаете копирования строк, где будет выполняться перемещение. Я не уверен, что соответствующий компилятор С++ 11 будет генерировать эквивалентный код, если вы полностью опустите все конструкторы copy/move, извините.
(Другими словами, я не уверен, что std::move
специально определен для элементарного перемещения для массивов.)
Ответ 3
Для шаблона класса, который вы написали, нет никакого преимущества в создании конструктора перемещения.
Было бы преимущество, если бы массив элементов был распределен динамически. Но с простым массивом в качестве члена нет ничего оптимизированного, вы можете копировать только значения. Невозможно переместить их.
Ответ 4
Обычно move-semantic реализуется, когда ваш класс управляет ресурсом. Поскольку в вашем случае класс не управляет ресурсом, семантика move будет больше похожа на семантику семантики, так как ничего не нужно перемещать.
Чтобы лучше понять, когда возникает необходимость в семантике перемещения, рассмотрите возможность создания _nums
указателя вместо массива:
template<unsigned int N>
class Foo {
public:
Foo()
{
_nums = new int[N](); //allocate and zeo-initialized
}
Foo(const Foo<N>& other)
{
_nums = new int[N];
for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
}
Foo(Foo<N>&& other)
{
_nums = other._nums; //move the resource
other._nums=0; //make it null
}
Foo<N> operator=(const Foo<N> & other); //implement it!
virtual ~Foo() { delete [] _nums; }
private:
int *_nums;
};