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

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

Ответ 1

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

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);
}

В любом случае программист С++ для путешествующих должен знать, что под капотом move больше ничего не происходит, кроме броска. И начинающий программист на С++, по крайней мере с 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);
}

В С++ 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&&>, но вы считаете, что лучше всего, и, возможно, вы закончите разработку новой лучшей практики (С++ постоянно развивается).

Итак, что делает 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 const&) вызывается X::operator=(X&&).

И это все, что есть std::move! Он полностью исчезает во время выполнения. Его единственное влияние - во время компиляции, когда он может изменить вызвавшую перегрузку.

Ответ 2

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

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

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