C ++ 11 std::move(x)
функция на самом деле ничего не двигает вообще. Это просто приведение к r-значению. Почему это было сделано? Разве это не вводит в заблуждение?
Правильно что 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&&>
и теперь код довольно наводит на мысль, почему эти приведения присутствуют: чтобы включить семантику перемещения!
Нужно понимать, что в контексте истории мало кто в этот момент действительно понимал тесную связь между значениями и семантикой перемещения (хотя в статье также это попыталось объяснить):
Семантика перемещения автоматически вступит в игру, когда получит значение
аргументы. Это совершенно безопасно, потому что перемещение ресурсов из
Значение не может быть замечено остальной частью программы (никто не имеет
ссылка на r-значение, чтобы обнаружить разницу).
Если в то время 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
будут проинформированы о том, что переехать от RHS, в отличие от копия от rhs, даже если они точно не понимают как это достигнуто.
Кроме того, если программист желает эту функциональность под другим именем, 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
к значению, а затем это значение 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
! Он полностью исчезает во время выполнения. Единственное его влияние — во время компиляции, когда может быть изменить то, что называется перегрузка.
Позвольте мне оставить здесь цитату из C ++ 11 FAQ написанный Б. Страуструпом, который является прямым ответом на вопрос ОП:
move (x) означает «вы можете рассматривать x как значение». Может быть, это будет иметь
было бы лучше, если бы Move () был вызван rval (), но теперь Move () имеет
был использован в течение многих лет.
Кстати, мне очень понравился FAQ — его стоит прочитать.