В 2022 году всё ещё существует много компаний, для которых C++11 считается новым стандартом языка. С людьми лучше, множество конференций, статей, книжек, гайдов на ютубе танцев в тиктоке неплохо объяснили идеи и рапространили знания. Для новоприбывших же, возможно, дерево типов ссылок в C++, разные приколы и эффекты могут повергнуть в уныние. Потому, здесь я покажу без особых теоретических запилов как в 80% случаев надо пользоваться семантикой перемещения в версиях языка C++11 и новее. Если вы душный дед из твитора и испытываете желание самоудовлетвориться самоутвердиться, что теория лёгкая, и вы её за две минуты освоили в третьем классе на батином инженерном калькуляторе, то вы ошиблись дверью, хабрахабр дальше по коридору. Остальным велкам.
Если же вы видите, что я где-то косякнул или что-то забыл упомянуть, пишите в телегу мне.
Типы конструкторов и подготовка
Здесь нам понадобятся два типа ссылок: обычная ссылка lvalue, которая может стоять слева (и справа) от присваивания (а значит имеет имя), и ссылка rvalue, которая может находиться только справа. Значениями типа rvalue являются результаты вызова функций, литералы.
Начнём, пожалуй с того, что напишем пару килограммов бойлерплейта (нудного, объемного, жизненно необходимого, но весьма скучного кода). Будем использовать для экспериментов вот такой класс:
Что произошло? Не впадая в мерзкое упрощение с целыми числами, мы завели класс со строчкой и вектором строчек внутри. Для наглядности. Явно реализовали конструктор по умолчанию (без аргументов), пользовательский конструктор (с аргументами) и деструктор. Явно написали копирующие и перемещающие операции. Опять же, чтобы не вспоминать сейчас правило трёх пяти, и что там когда компилятор нам генерирует. Про это потом.
Перемещающие операции отличаются синтаксически лишь двойным амперсандом (так обозначается rvalue ссыль) в списке параметров и отсутствием const
. И вызовами std::move()
, которая во время компиляции приводит тип аргумента как раз к нужной нам rvalue. Для встроенных типов в любом случае произойдёт копирование, для них недорогостоящее. Для обьектов, которые не содержат перемещающего конструктора, попытка перемещение так же будет заменена копированием. Так же есть шансы нарваться на копирование, если неправильно написаны и перемещающие операции. Перемещающие конструктор и оператор присваивания принимают аргумент rvalue, то есть с &&, но в теле функции оно становится lvalue, так как мы уже дали имя формальному параметру. Короче, для него в теле надо снова вызывать std::move()
, если хочется его куда-нибудь переместить или нужет rvalue от него.
Как передать и принять аргументы
В объявлении функции f(ClassName a, ClassName const b, ClassName &c, ClassName const& d, ClassName &&e);
первые два аргумента принимаются по значению, с вызовами копирующих конструкторов для T, вторые два lvalue ссылки, и последний - rvalue ссылка. С lvalue ссылками всё тут понятно, остаётся только внимательно проследить за временем жизни ссылок, чтобы ненароком кто-то другой (другой поток, обработчик исключения или системного сигнала) не удалил оригинал и у нас не образовалась висячая ссылка.
Передача по значению, приём по значению
Рассмотрим первый и последний случаи. Приём по значению, вызов возможно дорогих копирующих конструкторов, долго.
Передача rvalue, приём по значению
Приняли аргумент по значению, вроде должно произойти копирование, но нет. Передавали-то мы rvalue ссылку, и произошло воровство. Теперь ооригинальный обьект находится в теле вызыванной функции, а в вызывающей… А в вызывающей по разному. Вообще это UB, пользоваться обьектом из которого было выполнено перемещение, и на разных компиляторах может быть разное поведение.
Приём rvalue
В случае, когда же у нас в списке параметров стоит ClassName &&arg
, мы передаём ссылку. Сам обьект всё ещё остётся принадлежать вызывающему коду. И точно так же как и с lvalue ссылками можно нарваться на повисание этой ссылки. Например в одной функции обьект будет удален, или из него будет выполнено перемещение, как в предыдущем разделе. И всё, привет UB в другой функции.
Всё будет норм, если мы просто попользуемся ссылкой или присвоим её локальной переменной без использования std::move
– там вызовется копирующий конструктор, и ссылка останется действительной.
Что ещё
Здесь в примерах нет никакого выведения типов и, соответственно perfect forwarding’а. Так же предполагается, что запускается всего один поток, с многопоточностью там больше приседаний. Про это потом.
Код примеров можно посмотреть тут Ещё можно глянуть вот этот доклад с C++ Russia 2019