Перемещение конструкторов и статических массивов
Я изучал возможности Move конструкторов в C++, и мне было интересно, каковы некоторые способы использования этой функции в Примере, таком как ниже. Рассмотрим этот код:
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;
}
В приведенном выше примере, если мы проследим программу (с MSVC++ 2011), мы увидим, что Foo<N>::Foo(Foo<N>&&)
вызывается при построении foo
, что является желаемым поведением. Однако, если бы у нас не было Foo<N>::Foo(Foo<N>&&)
, Foo<N>::Foo(const Foo<N>&)
был бы вызван вместо этого, что сделало бы избыточную копию операция.
Мой вопрос заключается в том, как отмечено в коде, в этом конкретном примере, использующем статически выделенный простой массив, есть ли способ использовать конструктор move, чтобы избежать этой избыточной копии?
4 ответа:
Во-первых, есть общий совет, который говорит, что вы не должны писать никакой конструктор копирования/перемещения, оператор присваивания или деструктор вообще, если вы можете помочь ему, а скорее составить свой класс высококачественных компонентов, которые в свою очередь обеспечивают их, позволяя сгенерированным по умолчанию функциям делать правильные вещи. (Обратный вывод состоит в том, что если вы должны написать любой из них, то, вероятно, вам придется написать их все.)
Таким образом, вопрос сводится к "который класс компонентов с одной ответственностью может воспользоваться преимуществами семантики перемещения?"Общий ответ: Все, что управляет ресурсом . Дело в том, что конструктор/назначитель перемещения просто повторно установит ресурс в новый объект и аннулирует старый, таким образом избегая (предположительно дорогостоящего или невозможного) нового выделения и глубокого копирования ресурса.
Основным примером является все, что управляет динамической памятью, где операция перемещения просто копирует указатель и устанавливает указатель старого объекта в ноль (так что деструктор старого объекта ничего не делает). Вот наивный пример:
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 };
Ключ заключается в том, что любой конструктор копирования/перемещения будет выполнять полное копирование переменных-членов; только когда эти переменные сами являются дескрипторами или указателями на ресурсы, вы можете избежать копирования ресурса из-за соглашения, что перемещаемый объект больше не считается допустимым и что вы можете украсть его. Если нечего красть, значит, нечего и красть. преимуществом в движении.
В данном случае это бесполезно, потому что
int
не имеет конструкторов перемещения.Однако было бы полезно, если бы это были строки, например:
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]; };
Теперь вы избегаете копирования строк, где будет сделано перемещение. Я не уверен, что соответствующий компилятор C++11 будет генерировать эквивалентный код, если вы полностью опустите все конструкторы копирования/перемещения, извините.
(другими словами, Я не уверен, что
std::move
специально определен для выполнения элементного перемещения для массивов.)
Для шаблона класса, который вы написали, нет никаких преимуществ в конструкторе перемещения.
Было бы преимуществом, если бы массив элементов выделялся динамически. Но с простым массивом в качестве члена, нет ничего, чтобы оптимизировать, вы можете только копировать значения. Нет никакого способа переместить их.
Обычно move-semantic реализуется, когда ваш класс управляет ресурс . Поскольку в вашем случае класс не управляет ресурсом, семантика перемещения будет больше похожа на семантику копирования, поскольку нет ничего, что можно было быпереместить .
Чтобы лучше понять, когда move-semantic становится необходимым, рассмотрите возможность создания
_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; };