Семантика перемещения в деталях
Красть ресурсы вместо копирования - и делать это безопасно
std::move, компилятор может выбрать дешёвый путь перемещения вместо дорогого копирования. При грамотном применении семантика перемещения превращает глубокие копии за O(n) в обмен указателями за O(1) - и вызывающий код при этом не меняется.
- читать категории значений и точно знать, когда связывается rvalue-ссылка
T&&; - писать корректные конструктор перемещения и оператор перемещающего присваивания;
- объяснять, почему пометка
noexceptменяет производительность программы, а не только её обещания; - применять правило пяти (и предпочитать правило нуля);
- рассуждать о состоянии после перемещения, не вызывая неопределённого поведения.
1. Категории значений и rvalue-ссылки
У каждого выражения в C++ есть и тип, и категория значения. Для практической семантики перемещения важно разделение на lvalue - сущности с именем и устойчивым адресом, который можно взять через &, - и rvalue - временные и другие объекты, чьи ресурсы безопасно забирать, потому что их больше никто не увидит.
rvalue-ссылка, записываемая как T&&, связывается только с rvalue. В этом весь фокус: перегрузка с параметром T&& выбирается именно тогда, когда аргумент - истекающее значение, поэтому внутри такой перегрузки красть его ресурсы безопасно.
void f(const std::string& s); // перегрузка для lvalue: берёт что угодно, красть нельзя
void f(std::string&& s); // перегрузка для rvalue: берёт временные, красть можно
std::string name = "Ada";
f(name); // вызовет f(const std::string&) - name это lvalue
f("Ada"); // вызовет f(std::string&&) - временное это rvalue
f(std::move(name)); // вызовет f(std::string&&) - ОБЕЩАЕМ больше не трогать name
std::move ничего не перемещает. Это приведение типа: он выдаёт rvalue-ссылку на свой аргумент, чтобы была выбрана перемещающая перегрузка. Сама работа происходит внутри выбранного конструктора или оператора перемещения. Считайте std::move фразой "относись к этому как к истекающему" - перемещает сам тип, а не приведение.
void g(std::string&& s) параметр s имеет имя, поэтому, чтобы переместить из него, снова нужно написать std::move(s). Забыв это, вы молча получите копию.