Внедрение Move Constructor путем вызова оператора назначения переноса
Статья MSDN, Как написать Write Constuctor, имеет следующую рекомендацию.
Если вы предоставляете как конструктор перемещения, так и оператор назначения перемещения для своего класса, вы можете исключить избыточный код, написав конструктор перемещения, чтобы вызвать оператор назначения перемещения. в следующем примере показана измененная версия конструктора перемещения, которая вызывает оператор присваивания перемещения:
// Move constructor.
MemoryBlock(MemoryBlock&& other)
: _data(NULL)
, _length(0)
{
*this = std::move(other);
}
Является ли этот код неэффективным, дважды инициализируя значения MemoryBlock
, или компилятор сможет оптимизировать дополнительные инициализации? Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?
Ответы
Ответ 1
Я бы так не сделал. Причиной появления элементов перемещения в первую очередь является производительность. Выполнение этого для вашего конструктора движений - это как обстрел мегабайков для суперкара, а затем попытка сэкономить деньги, покупая регулярный газ.
Если вы хотите уменьшить количество написанного кода, просто не записывайте элементы перемещения. Ваш класс будет копировать только отлично в контексте перемещения.
Если вы хотите, чтобы ваш код имел высокую производительность, а затем создайте конструктор перемещения и переместите назначение как можно быстрее. Хорошие участники движения будут стремительно быстрыми, и вы должны оценивать их скорость, подсчитывая нагрузки, магазины и ветки. Если вы можете написать что-то с 4 загрузками/магазинами вместо 8, сделайте это! Если вы можете написать что-то без ветвей вместо 1, сделайте это!
Когда вы (или ваш клиент) помещаете свой класс в std::vector
, много движений может генерироваться в вашем типе. Даже если ваш ход будет молниеносно в 8 загрузочных/хранилищах, если вы можете сделать его в два раза быстрее или даже на 50% быстрее только с 4 или 6 загрузками/магазинами, imho, потраченное на время.
Лично мне надоело видеть ожидающих курсоров, и я желаю пожертвовать дополнительные 5 минут на то, чтобы написать свой код и знать, что он как можно быстрее.
Если вы все еще не уверены, что это того стоит, напишите его в обоих направлениях, а затем просмотрите сгенерированную сборку при полной оптимизации. Кто знает, ваш компилятор просто может быть достаточно умным, чтобы оптимизировать дополнительные нагрузки и магазины для вас. Но к этому времени вы уже вложили больше времени, чем если бы вы только что создали оптимизированный конструктор перемещения.
Ответ 2
Моя версия С++ 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));
}
С правильным компилятором С++ 11 MemoryBlock::MemoryBlock(size_t)
следует вызывать только 3 раза в тестовой программе.
Ответ 3
Я не думаю, что вы заметите значительную разницу в производительности. Я считаю хорошей практикой использовать оператор присваивания перемещения из конструктора перемещения.
Однако я предпочел бы использовать std:: forward вместо std:: move, потому что это более логично:
*this = std::forward<MemoryBlock>(other);
Ответ 4
[...] сможет ли компилятор оптимизировать лишние инициализации?
Почти во всех случаях: да.
Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?
Да, просто реализуйте это с помощью оператора назначения перемещения, за исключением случаев, когда вы измерили, что это приводит к неоптимальной производительности.
Сегодня оптимизатор делает невероятную работу по оптимизации кода. Ваш пример кода особенно легко оптимизировать. Прежде всего: конструктор перемещения будет встроен почти во всех случаях. Если вы реализуете это с помощью оператора присваивания перемещения, он также будет встроен.
И давайте посмотрим на некоторые сборки! Здесь показан точный код с веб-сайта 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, выполняет больше работы, чем конструктор перемещения: он защищен от самостоятельного назначения. Конструктор перемещения не защищен от этого. Но: как я уже сказал, конструктор будет встроен практически во всех случаях. И в этих случаях оптимизатор может видеть, что это не самостоятельное назначение, поэтому эта ветвь также будет оптимизирована.
Это часто повторяется, но важно: не делайте преждевременной микро -O -птимизации!
Не поймите меня неправильно, я также ненавижу программное обеспечение, которое тратит много ресурсов из-за ленивых или небрежных разработчиков или управленческих решений. Экономия энергии - это не только батареи, но и экологическая тема, которой я очень увлечен. Но преждевременное проведение микро -O -пистилизаций в этом отношении не помогает! Конечно, сохраняйте алгоритмическую сложность и удобство кэширования ваших больших данных в своей голове. Но прежде чем делать какую-либо конкретную оптимизацию, измерьте!
В этом конкретном случае я бы даже предположил, что вам никогда не придется оптимизировать вручную, потому что компилятор всегда сможет генерировать оптимальный код вокруг вашего конструктора перемещения. Выполнение бесполезной микро--O -птимизации сейчас обойдется вам позже, когда вам нужно изменить код в двух местах или когда вам нужно отладить странную ошибку, которая возникает только потому, что вы изменили код только в одном месте. И это потраченное впустую время разработки, которое можно потратить на полезную оптимизацию.
Ответ 5
Это зависит от вашего оператора присваивания. Если вы посмотрите на ту, в которой вы связаны, вы частично видите:
// Free the existing resource.
delete[] _data;
Итак, в этом контексте, если вы сначала вызвали оператор присваивания перемещения из конструктора move без инициализации _data
, вы в конечном итоге попытаетесь удалить неинициализированный указатель. Таким образом, в этом примере, неэффективно или нет, на самом деле важно, чтобы вы инициализировали значения.
Ответ 6
Я просто исключил бы инициализацию члена и напишу,
MemoryBlock(MemoryBlock&& other)
{
*this = std::move(other);
}
Это всегда будет работать, если исключение переноса не приведет к исключениям, и обычно это не так!
Преимущества этих стилей:
- Вам не нужно беспокоиться о том, будет ли компилятор дважды инициализировать члены, потому что это может различаться в разных средах.
- Вы пишете меньше кода.
- Вам не нужно обновлять его, даже если вы добавите дополнительных членов в класс в будущем.
- Компиляторы часто могут встраивать назначение перемещения, поэтому накладные расходы на конструктор копирования будут минимальными.
Я думаю, что сообщение @Howard не совсем отвечает на этот вопрос. На практике классы часто не любят копирование, многие классы просто отключают конструктор копирования и назначение копии. Но большинство классов могут быть перемещаемыми, даже если они не могут быть скопированы.