Пусть x
- переменная некоторого типа, которая была ранее инициализирована. Является следующей строкой:
x = std::move(x)
undefined? Где это в стандарте и что он говорит об этом?
Пусть x
- переменная некоторого типа, которая была ранее инициализирована. Является следующей строкой:
x = std::move(x)
undefined? Где это в стандарте и что он говорит об этом?
Нет, это не поведение undefined, это будет определенное поведение реализации, оно будет зависеть от того, как реализовано назначение переноса.
Соответственно этому LWG issue 2468: Self-move-назначение типов библиотек, обратите внимание, что это активная проблема и не имеет официального предложения, поэтому это должно считаются ориентировочными, а не окончательными, но он указывает разделы, которые участвуют в стандартной библиотеке, и указывает, что они в настоящее время конфликтуют. В нем говорится:
Предположим, что мы пишем
vector<string> v{"a", "b", "c", "d"}; v = move(v);
Каким должно быть состояние v? В стандарте ничего не сказано специфические для самодвижения-назначения. Там соответствующий текст в нескольких части стандарта, и неясно, как их согласовать.
[...]
Из текста не видно, как свести эти штуки вместе, потому что не ясно, какой из них имеет преимущество. Возможно, 17.6.4.9 [res.on.arguments] выигрывает (он налагает неявное предварительное условие, которое не упоминается в требованиях MoveAssignable, поэтому v = move (v) есть undefined), или, может быть, 23.2.1 [container.requirements.general] (он явно дает дополнительные гарантии для Container:: operator = за пределами того, что гарантировано для библиотечных функций в целом, поэтому v = move (v) является no-op) или, возможно, что-то еще.
В существующих реализациях, которые я проверил, для чего это стоит, v = move (v) появился, чтобы очистить вектор; он не оставил вектор неизменным и не вызвал крушения.
и предлагает:
Неофициально: измените таблицы требований MoveAssignable и Container (и любые другие таблицы требований, которые упоминают назначение переноса, если они есть), чтобы сделать его явным, что x = move (x) определено поведение и оно оставляет x в действительном, но неуказанном состоянии, Вероятно, это не то, что сегодня говорится в стандарте, но это, вероятно, то, что мы намеревались, и оно согласуется с тем, что мы сказали пользователям и с какими реализациями на самом деле.
Примечание. Для встроенных типов это в основном копия, мы можем видеть из стандартной секции С++ 14 5.17
[expr.ass]:
В простом присваивании (=) значение выражения заменяет значение объекта, на которое ссылается левый операнд.
который отличается от случая для классов, где 5.17
говорит:
Если левый операнд имеет тип класса, класс должен быть полным. Назначение объектам класса определено оператором присваивания копирования/перемещения (12.8, 13.5.3).
Примечание. clang имеет предупреждение о перемещении.
Он вызывается X::operator = (X&&)
, поэтому для реализации этого случая требуется реализация (как это делается для X::operator = (const X&)
)
Все, что делает, это вызов X::operator=(X&&)
(с lvalue, квалифицированным "*this
" ).
В примитивных типах std::move
мало интересует и вообще не взаимодействует с =
. Таким образом, это относится только к объектам типа класса.
Теперь для типа внутри std
(или сгенерированного одним из его шаблонов) объекты move
d из имеют тенденцию оставаться в неуказанном (но действительном) состоянии. Это не поведение undefined, но это не полезно.
Семантика каждого заданного X::operator=(X&&)
должна быть рассмотрена, рассмотрение каждого типа внутри std
будет "слишком широким" для ответа на переполнение стека. Они даже могут противоречить себе.
В общем случае, когда move
ing от объекта, вы сообщаете потребителю, что "вам не важно, в каком состоянии находится объект после". Использование x = std::move(x)
является таким образом невежливым, поскольку вы (как правило) заботитесь о том, какое состояние x
находится после завершения операции (как вы его назначаете). Вы используете тот же объект, что и lvalue, и rvalue в пределах одной и той же операции, и это не является хорошей практикой.
Интересным исключением является значение по умолчанию std::swap
, которое выглядит примерно так:
template<class T>
void swap(T& lhs, T& rhs) {
T tmp = std::move(lhs);
lhs = std::move(rhs);
rhs = std::move(tmp);
}
средняя строка, lhs = std::move(rhs)
, делает x = std::move(x)
, если вы дважды вызываете swap на одном и том же объекте.
Обратите внимание, однако, что нам все равно, какое состояние x
находится после завершения этой строки; мы уже сохранили состояние x
в tmp
, и мы восстановим его на следующей строке.