Представьте вот такой код:
auto object = GetObject(params...);
auto another = object;
MessAround(object);
// (1) you want to write some logic here
Где
MessAround — функция, которая непредсказуемым образом меняет значение вашего object. То есть значение соответствует всем инвариантам, но вы не знаете, какое конкретно. Как вы будете писать код в (1)?
Вы наверное проверите какие-то свойства вашего
object. Может он там empty()/isNull()/valid(). Или может вы сразу решите его почистить: clear(). Или может присвоить что-нибудь туда захотите: object = Object{1, 2, 3}.Или можете вообще сравнить его с чем-то (вдруг там всё-таки какое-то конкретное значение появилось):
if (object == "str_val"_obj) {...}
Делаете ли вы что-то неправильное? Должен ли компилятор вам подсказать проблему в таком коде? Может UBsan? Может clang-tidy?
Да нет.
Более того, даже если вы сделаете что-то такое:
auto first = object["first"];
Вы возможно ничего плохого не сделали. Вы просто не знаете, что точно там лежит.
MessAround туда могло подложить что угодно. Конечно, проблемы могут возникнуть, если вы, не проверив состояние вашего объекта, будете предполагать, что он обладает какими-то свойствами. Например, думать, что он не пустой. Или что он содержит сколько-то каких-то элементов. Это всё может быть, но вообще-то мы точно утверждать не можем.
Использовать объект после
MessAround это примерно то же самое, что написать функцию, решающую некоторую задачу в вакууме. На примере функции, решающей любую задачу:
void solve(Object obj) {...}
Вы опять же про этот
obj ничего не знаете. Вам надо проверить разные корнер кейсы. А потом как-то там решать вашу проблему. Чем эти примеры отличаются от работы с переменной после
std::move? Да ничем.
Мы так яро привыкли к правилу «использовать переменную после
std::move опасно», что в нашей ментальной модели это часто равносильно undefined behaviour. Уже само это действие вызывает подозрения. Code smell так называемый. Хотя никакого UB тут нет. UB может возникнуть от того, что вы пользуетесь объектом, предполагая, что у него есть какие-то свойства. Точно так же и в нашей функции
solve, которая может не учесть какие-то потенциальные входные данные. Пытаться разыменовать пустой указатель, не проверив его, неправильно. И к сожалению эта ошибка стала достаточно частой, чтобы мы выработали ощущение неправильности происходящего. Само действие стало табу. В clang-tidy вон проверка отдельная есть (которую приходится игнорировать с
NOLINT). Отсюда рождаются ментальные модели вида
• «после std::move ничего нельзя»
• «после std::move можно только переприсваивать или уничтожать объект, всё остальное запрещено».
Это упрощения, которые мы себе придумали, чтобы меньше думать. И они почти всегда работают. Но иногда всё же нет.
Давайте думать и не вестись на вот эти попытки наших развитых эволюционировавших мозгов упрощать. Это мы профессионально ленимся.
P.S. Важно подчеркнуть ещё один факт.
Состояние объекта после
std::move — всем известное «valid but unspecified». По стандарту это работает для стандартной библиотеки. Но кажется, мы уже настолько к этому привыкли, что считаем это данностью в любом случае. Не то чтобы это ожидание обосновано. Авторы библиотек делают что хотят. Но наверное писать ломающий это ожидание код всё-таки не надо. А для стандартной библиотеки довольно просто понять, можно ли вызывать метод у мувнутого объекта: у метода не должно быть precondition. Например у
std::vector::front он есть: !empty(); у std::vector::clear такого нет. @thisnotes. Patreon, newsletter.
Спасибо Artyom Garkavy и niki4smirn.