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

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

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

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

Семантика перемещения автоматически вступает в игру, когда передается аргумент rvalue. Это совершенно безопасно, потому что остальная часть программы не может заметить перемещение ресурсов из значения r (никто не имеет ссылки на значение 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, будут проинформированы, что намерение состоит в том, чтобы перейти от прав, а не копировать с прав, даже если они не понимают, как именно это достигается.

Кроме того, если программист желает эту функциональность под другим именем, 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, написанную Б. Страуструпом, которая является прямым ответом на вопрос ОП:

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

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

Другие вопросы по тегам