Статья MSDN, Как: написать конструктор Move, имеет следующую рекомендацию.
Если вы предоставляете для своего класса и конструктор перемещения, и оператор присваивания перемещения, вы можете устранить избыточный код, написав
конструктор перемещения для вызова оператора присваивания перемещения.
В следующем примере показана исправленная версия конструктора перемещения, который
вызывает оператора назначения перемещения:
// Move constructor.
MemoryBlock(MemoryBlock&& other)
: _data(NULL)
, _length(0)
{
*this = std::move(other);
}
Является ли этот код неэффективным при двойной инициализации MemoryBlock
значения, или компилятор сможет оптимизировать дополнительные инициализации? Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?
Я бы так не поступил. Причиной существования участников переезда является спектакль. Делать это для вашего ходового конструктора — все равно, что выкладывать мегабаксы на суперкар, а затем пытаться сэкономить, покупая обычный газ.
Если вы хотите уменьшить объем написанного кода, просто не пишите элементы перемещения. Ваш класс будет просто копировать в контексте перемещения.
Если вы хотите, чтобы ваш код был высокопроизводительным, то настройте конструктор перемещения и назначьте перемещение как можно быстрее. Хорошие участники движения будут невероятно быстрыми, и вы должны оценивать их скорость, считая нагрузки, магазины и филиалы. Если вы можете написать что-то с 4 загрузками / хранилищами вместо 8, сделайте это! Если вы можете написать что-то без веток вместо 1, сделайте это!
Когда вы (или ваш клиент) поместите свой класс в std::vector
, много ходов может генерироваться по вашему типу. Даже если ваш ход молниеносный при 8 загрузках / магазинах, если вы можете сделать это в два раза быстрее, или даже на 50% быстрее всего лишь с 4 или 6 загрузками / магазинами, imho — это хорошо проведенное время.
Лично я устал от ожидания курсоров и готов потратить дополнительные 5 минут на написание своего кода и знаю, что это как можно быстрее.
Если вы все еще не уверены, что это того стоит, запишите это в обоих направлениях, а затем изучите сгенерированную сборку при полной оптимизации. Кто знает, ваш компилятор может быть достаточно умен, чтобы оптимизировать дополнительные нагрузки и хранилища для вас. Но к этому времени вы уже потратили больше времени, чем если бы вы только что написали оптимизированный конструктор перемещения.
Моя версия C ++ 11 MemoryBlock
учебный класс.
#include <algorithm>
#include <vector>
// #include <stdio.h>
class MemoryBlock
{
public:
explicit MemoryBlock(size_t length)
: length_(length),
data_(new int[length])
{
// printf("allocating %zd\n", length);
}
~MemoryBlock() noexcept
{
delete[] data_;
}
// copy constructor
MemoryBlock(const MemoryBlock& rhs)
: MemoryBlock(rhs.length_) // delegating to another ctor
{
std::copy(rhs.data_, rhs.data_ + length_, data_);
}
// move constructor
MemoryBlock(MemoryBlock&& rhs) noexcept
: length_(rhs.length_),
data_(rhs.data_)
{
rhs.length_ = 0;
rhs.data_ = nullptr;
}
// unifying assignment operator.
// move assignment is not needed.
MemoryBlock& operator=(MemoryBlock rhs) // yes, pass-by-value
{
swap(rhs);
return *this;
}
size_t Length() const
{
return length_;
}
void swap(MemoryBlock& rhs)
{
std::swap(length_, rhs.length_);
std::swap(data_, rhs.data_);
}
private:
size_t length_; // note, the prefix underscore is reserved.
int* data_;
};
int main()
{
std::vector<MemoryBlock> v;
// v.reserve(10);
v.push_back(MemoryBlock(25));
v.push_back(MemoryBlock(75));
v.insert(v.begin() + 1, MemoryBlock(50));
}
С правильным C ++ 11 компилятором, MemoryBlock::MemoryBlock(size_t)
следует вызывать только 3 раза в тестовой программе.
Я не думаю, что вы заметите существенную разницу в производительности. Я считаю хорошей практикой использовать оператор присваивания перемещения из конструктора перемещения.
Однако я бы предпочел использовать std :: forward вместо std :: move, потому что это более логично:
*this = std::forward<MemoryBlock>(other);
[…] сможет ли компилятор оптимизировать лишние инициализации?
Почти во всех случаях: да.
Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?
Да, просто реализуйте это с помощью оператора присваивания перемещения, кроме случаев, когда вы измеренный что это приводит к неоптимальной производительности.
Сегодняшний оптимизатор делает невероятную работу по оптимизации кода. Ваш пример кода особенно легко оптимизировать. Прежде всего: конструктор перемещения будет встроен почти во всех случаях. Если вы реализуете это с помощью оператора присваивания перемещения, он также будет встроен.
И давайте посмотрим на некоторые сборки! это показывает точный код с веб-сайта Microsoft с обеими версиями конструктора перемещения: вручную и с помощью назначения перемещения. Вот выход сборки для GCC с -O
(-O1
имеет такой же выходной сигнал; вывод clang приводит к тому же выводу):
; ===== manual version ===== | ; ===== via move-assig =====
MemoryBlock(MemoryBlock&&): | MemoryBlock(MemoryBlock&&):
mov QWORD PTR [rdi], 0 | mov QWORD PTR [rdi], 0
mov QWORD PTR [rdi+8], 0 | mov QWORD PTR [rdi+8], 0
| cmp rdi, rsi
| je .L1
mov rax, QWORD PTR [rsi+8] | mov rax, QWORD PTR [rsi+8]
mov QWORD PTR [rdi+8], rax | mov QWORD PTR [rdi+8], rax
mov rax, QWORD PTR [rsi] | mov rax, QWORD PTR [rsi]
mov QWORD PTR [rdi], rax | mov QWORD PTR [rdi], rax
mov QWORD PTR [rsi+8], 0 | mov QWORD PTR [rsi+8], 0
mov QWORD PTR [rsi], 0 | mov QWORD PTR [rsi], 0
| .L1:
ret | rep ret
Помимо дополнительной ветки для нужной версии, код точно такой же. Имея в виду: дубликаты заданий были удалены.
Почему дополнительная ветка? Оператор назначения перемещения, определенный страницей Microsoft, выполняет больше работы, чем конструктор перемещения: он защищен от самостоятельного назначения. Конструктор перемещения не защищен от этого. Но: как я уже сказал, конструктор будет встроен практически во всех случаях. И в этих случаях оптимизатор может видеть, что это не самостоятельное назначение, поэтому эта ветвь также будет оптимизирована.
Это повторяется много раз, но это важно: не делайте преждевременную микрооптимизацию!
Не поймите меня неправильно, я также ненавижу программное обеспечение, которое тратит много ресурсов из-за ленивых или небрежных разработчиков или управленческих решений. Экономия энергии — это не только батареи, но и экологическая тема, которой я очень увлечен. Но, преждевременная микрооптимизация не помогает в этом отношении! Конечно, сохраняйте алгоритмическую сложность и удобство кэширования ваших больших данных в своей голове. Но прежде чем делать какую-либо конкретную оптимизацию, измерение!
В этом конкретном случае я бы даже предположил, что вам никогда не придется оптимизировать вручную, потому что компилятор всегда сможет генерировать оптимальный код вокруг вашего конструктора перемещения. Выполнение бесполезной микрооптимизации сейчас будет стоить вам времени на разработку позже, когда вам нужно изменить код в двух местах или когда вам нужно отладить странную ошибку, которая возникает только потому, что вы изменили код только в одном месте. И это потраченное впустую время разработки, которое можно потратить на полезную оптимизацию.
Это зависит от того, что делает ваш оператор присваивания. Если вы посмотрите на ту статью, на которую вы ссылаетесь, вы увидите частично:
// Free the existing resource.
delete[] _data;
Так что в этом контексте, если вы вызвали оператор присваивания перемещения из конструктора перемещения без инициализация _data
во-первых, вы попытаетесь удалить неинициализированный указатель. Так что в этом примере, неэффективно или нет, на самом деле очень важно, чтобы вы инициализировали значения.
Я бы просто исключил инициализацию членов и написал:
MemoryBlock(MemoryBlock&& other)
{
*this = std::move(other);
}
Это всегда будет работать, если только при назначении перемещения не генерируются исключения, и обычно это не так!
Преимущества этого стиля:
Я думаю, что сообщение @ Говарда не совсем отвечает на этот вопрос. На практике классы часто не любят копирование, многие классы просто отключают конструктор копирования и назначение копирования. Но большинство классов могут быть перемещаемыми, даже если они не копируются.