Верно , что std::move(x)
это просто приведение к rvalue - точнее к xvalue , а не к prvalue . Верно и то, что наличие названного актерского состава move
иногда сбивает людей с толку. Однако цель этого наименования - не запутать, а скорее сделать ваш код более читабельным.
История компании move
восходит к первоначальному предложению о переезде в 2002 году . В этой статье сначала представлена ссылка на rvalue, а затем показано, как написать более эффективный std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Следует напомнить, что на данном этапе истории единственное, что &&
могло означать «», было логическим и . Никто не был знаком ни со ссылками на rvalue, ни с последствиями приведения lvalue к rvalue (без создания копии, как это static_cast<T>(t)
было бы). Поэтому читатели этого кода, естественно, подумают:
Я знаю, как swap
должно работать (скопировать на временное, а затем обменять значения), но какова цель этих уродливых слепков ?!
Также обратите внимание, что swap
на самом деле это просто замена для всех видов алгоритмов изменения перестановок. Это обсуждение намного больше, чем swap
.
Затем предложение вводит синтаксический сахар, который заменяет на static_cast<T&&>
что-то более читаемое, которое передает не точное, что , а, скорее, почему :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Т.е. move
это просто синтаксический сахар для static_cast<T&&>
, и теперь код довольно наводит на размышления о том, зачем нужны эти приведения: чтобы включить семантику перемещения!
Следует понимать, что в контексте истории немногие люди в этот момент действительно понимали тесную связь между rvalue и семантикой перемещения (хотя в статье это также пытается объяснить):
Семантика перемещения автоматически вступает в игру при задании аргументов rvalue. Это совершенно безопасно, потому что перемещение ресурсов из rvalue не может быть замечено остальной частью программы ( никто другой не имеет ссылки на rvalue, чтобы обнаружить разницу ).
Если бы в то время swap
было представлено так:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Тогда люди бы посмотрели на это и сказали:
Но почему вы используете rvalue?
Главный момент:
Как было, используя move
, ни разу никто не спросил:
Но почему вы переезжаете?
Шли годы, и предложение было уточнено, и понятия lvalue и rvalue были преобразованы в категории значений, которые мы имеем сегодня:
(изображение бессовестно украдены из dirkgently )
Итак, сегодня, если бы мы хотели swap
точно сказать, что он делает, а не почему , это должно быть больше похоже на:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
И вопрос, который каждый должен задать себе, заключается в том, является ли приведенный выше код более или менее читаемым, чем:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Или даже оригинал:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
В любом случае, опытный программист на C ++ должен знать, что под капотом move
не происходит ничего, кроме приведения типов . И начинающий программист на C ++, по крайней мере move
, будет проинформирован о том, что намерение состоит в том, чтобы перейти от правого, а не копировать с правого, даже если они не понимают, как именно это достигается.
Вдобавок, если программист желает использовать эту функциональность под другим именем, он не std::move
обладает монополией на эту функциональность, и в ее реализации не участвует непереносимая языковая магия. Например, если кто-то хочет кодировать set_value_category_to_xvalue
и использовать его вместо этого, это тривиально:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
В C ++ 14 он стал еще более кратким:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Так что, если вы так склонны, украсьте static_cast<T&&>
себя так, как считаете лучше, и, возможно, вы в конечном итоге разработаете новую передовую практику (C ++ постоянно развивается).
Так что же move
делать с точки зрения сгенерированного объектного кода?
Учтите это test
:
void
test(int& i, int& j)
{
i = j;
}
Скомпилировано с clang++ -std=c++14 test.cpp -O3 -S
, это создает этот объектный код:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Теперь, если тест изменить на:
void
test(int& i, int& j)
{
i = std::move(j);
}
В объектном коде нет абсолютно никаких изменений . Этот результат можно обобщить на: Для тривиально подвижных объектов std::move
не оказывает никакого влияния.
Теперь давайте посмотрим на этот пример:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Это генерирует:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
При запуске __ZN1XaSERKS_
через c++filt
это производит: X::operator=(X const&)
. Здесь нет ничего удивительного. Теперь, если тест изменить на:
void
test(X& i, X& j)
{
i = std::move(j);
}
Тогда в сгенерированном объектном коде по- прежнему нет никаких изменений . std::move
ничего не сделал, кроме приведения j
к rvalue, а затем это rvalue X
привязывается к оператору присваивания копии X
.
Теперь давайте добавим оператор присваивания перемещения к X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Теперь объектный код действительно меняется:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Запуск __ZN1XaSEOS_
через c++filt
показывает , что X::operator=(X&&)
вызывается вместо X::operator=(X const&)
.
И это все, что нужно std::move
! Он полностью исчезает во время выполнения. Его единственное влияние - во время компиляции, когда он может изменить вызываемую перегрузку.
std::move
самом деле