Почему `std::move` называется `std:: move`?


C++11 std::move(x) функция на самом деле ничего не перемещает. Это просто приведение к r-значению. Зачем это было сделано? Разве это не вводит в заблуждение?

2 112

2 ответа:

правильно, что 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&&>, и теперь код довольно наводит на размышления о том, почему эти приведения существуют: чтобы включить семантику перемещения!

нужно понимать, что в контексте истории мало кто в этот момент действительно понимал тесную связь между rvalues и семантикой перемещения (хотя в статье также пытаются объяснить это):

переместить семантика автоматически вступит в игру при заданном значении 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 были уточнены в категории стоимостью у нас сегодня:

Taxonomy

(изображение бесстыдно украдено из 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 do в терминах сгенерированного объекта код?

подумайте об этом 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);
}

здесь абсолютно никаких изменений в объектном коде. Этот результат можно обобщить на: For тривиально движимое объекты 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! Он полностью исчезает во время выполнения. Его влияние только во время компиляции, где он может изменить то, что перегрузка называется.

позвольте мне просто оставить здесь цитату из C++11 FAQ написано Б. Страуструпом, что является прямым ответом на вопрос ОП:

move (x) означает "вы можете рассматривать x как rvalue". Может быть, это было бы лучше, если двигаться() был вызван rval(), но теперь перейдем() имеет используется уже много лет.

кстати, мне очень понравился FAQ-его стоит прочитать.