При возврате объекта класса по значению из функции можно ли в современном C++ гарантировать отсутствие копирования, и от чего зависит ответ?
В C++17 и новее отсутствие копирования гарантируется, когда функция возвращает неименованный временный объект — результат создаётся сразу в объекте-результате вызова. Для именованной локальной переменной такое устранение копирования называется NRVO и остаётся разрешённой, но не гарантированной оптимизацией.
Следовательно, возврат временного объекта может работать даже без доступного копирующего или перемещающего конструктора, а возврат именованной локальной переменной обычно требует доступного перемещения или копирования.
До C++17 компилятор мог устранять копирование временных объектов как оптимизацию, даже если конструктор копирования имел наблюдаемые побочные эффекты. Это называлось copy elision, но формально оставалось разрешённой оптимизацией, а не гарантией.
В C++17 изменилась семантика prvalue: выражение, создающее временный объект, больше не обязано сначала материализовать отдельный временный объект, а затем копировать его. В целевых контекстах объект сразу создаётся в конечном месте назначения.
Код, возвращающий объект по значению, может выглядеть одинаково, но иметь разную семантику в зависимости от того, возвращается ли временное выражение или именованная переменная. Неверное предположение о гарантированном устранении копирования приводит к ошибкам компиляции, если копирующий и перемещающий конструкторы удалены или недоступны.
Дополнительная опасность — различать гарантированное устранение копирования и NRVO. Компилятор вправе применить NRVO, но программа не должна зависеть от него, если корректность требует наличия доступного конструктора копирования или перемещения.
При возврате выражения, создающего объект непосредственно, в C++17 и новее объект строится сразу в объекте-результате вызова функции. Отдельного временного объекта и последующего перемещения из него нет.
При возврате именованной локальной переменной компилятор может применить NRVO и построить эту переменную сразу в объекте-результате. Однако NRVO не гарантирован стандартом: например, сложная структура условий, разные возвращаемые переменные или особенности реализации могут помешать оптимизации.
Если NRVO не выполнен, возвращаемая локальная переменная является подходящим источником для перемещения. Сначала выбирается перемещающий конструктор, если он доступен; при его отсутствии может использоваться копирующий конструктор. Поэтому для именованного объекта необходим хотя бы один подходящий конструктор.
Важно, что требование доступности конструктора проверяется с учётом правил языка, даже если конкретный компилятор фактически устранит копирование. Гарантированная элиминация для prvalue — часть семантики языка, а NRVO — оптимизация, на которую нельзя полагаться как на обязательную.
Вызов direct корректен в C++17 и новее, поскольку отдельное копирование или перемещение не требуется. В named программа должна быть корректной и в случае, когда NRVO не применяется, поэтому удалённые конструкторы делают такой возврат некорректным.
В фабрике создавался тяжёлый объект конфигурации. Один вариант возвращал временный объект непосредственно, другой сначала создавал локальную переменную и возвращал её. Разработчик удалил копирование и перемещение, рассчитывая, что современный компилятор всегда применит оптимизацию.
Вариант с непосредственным временным объектом имеет простой и гарантированный результат в C++17+: объект создаётся сразу у вызывающей стороны. Вариант с локальной переменной потенциально также не выполняет копирование благодаря NRVO, но при изменении тела функции или на другом компиляторе может потребовать перемещающий конструктор.
Выбран был первый вариант для простого пути создания. Если же объект нужно выбирать между несколькими локальными переменными, конструктор перемещения оставляют доступным: это обеспечивает корректность при отсутствии NRVO. Результат — предсказуемая семантика без зависимости от уровня оптимизации.
1. Гарантируется ли устранение копирования при возврате именованной локальной переменной?
Нет. Это NRVO, и стандарт разрешает компилятору выполнить его, но не требует этого. Код должен оставаться корректным без NRVO, поэтому для именованной переменной обычно нужен доступный конструктор перемещения или копирования.
2. Чем возврат prvalue отличается от возврата xvalue через перемещение локального объекта?
Prvalue в гарантированном случае сразу создаёт конечный объект, поэтому конструктор перемещения не вызывается. Явное преобразование локальной переменной в xvalue лишь разрешает перемещение из неё и, наоборот, обычно мешает NRVO: компилятор уже видит не простой возврат именованной переменной, а другое выражение.
3. Требуется ли доступный деструктор при гарантированной элиминации копирования?
Да. Даже если копирующий и перемещающий конструкторы не нужны, деструктор возвращаемого типа должен быть доступен и не удалён в точке возврата. Созданный объект всё равно будет уничтожен в соответствии с обычными правилами времени жизни объектов.