Namespaces
Variants

Copy elision

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Когда выполняются определенные критерии, создание объекта класса из исходного объекта того же типа (игнорируя cv-квалификацию) может быть опущено, даже если выбранный конструктор и/или деструктор объекта имеют побочные эффекты. Это исключение создания объекта называется copy elision .

Содержание

Объяснение

Копирование может быть опущено в следующих случаях (которые могут быть объединены для устранения нескольких копирований):

  • В return statement функции с возвращаемым типом класса, когда операндом является имя не-volatile объекта obj с automatic storage duration (кроме параметра функции или параметра handler ), copy-initialization результирующего объекта может быть опущено путём непосредственного конструирования obj в результирующий объект вызова функции. Этот вариант copy elision известен как named return value optimization (NRVO).
  • Когда объект класса target копирующе-инициализируется с помощью временного объекта класса obj , который не был привязан к ссылке, копирующая инициализация может быть опущена путем непосредственного конструирования obj в target . Этот вариант пропуска копирования известен как оптимизация безымянного возвращаемого значения (URVO). Начиная с C++17, URVO является обязательной и больше не считается формой пропуска копирования; см. ниже.
(до C++17)
  • В throw выражении , когда операндом является имя не-volatile объекта obj с автоматической продолжительностью хранения (кроме параметра функции или параметра обработчика), который принадлежит области видимости , не содержащей самый внутренний охватывающий try блок (если существует), копирующая инициализация объекта исключения может быть опущена путем непосредственного конструирования obj в объект исключения.
  • В обработчике , копирующая инициализация аргумента обработчика может быть опущена путем трактовки параметра обработчика как псевдонима объекта исключения, если смысл программы останется неизменным за исключением выполнения конструкторов и деструкторов для аргумента обработчика.
(начиная с C++11)
  • В сопрограммах , копия параметра сопрограммы может быть опущена. В этом случае ссылки на эту копию заменяются ссылками на соответствующий параметр, если смысл программы останется неизменным за исключением выполнения конструктора и деструктора для объекта-копии параметра.
(начиная с C++20)

Когда происходит копирующее исключение, реализация рассматривает источник и цель опущенной инициализации как просто два различных способа обращения к одному и тому же объекту.

Уничтожение происходит в более поздний из моментов времени, когда два объекта были бы уничтожены без оптимизации.

(until C++11)

Если первый параметр выбранного конструктора является rvalue-ссылкой на тип объекта, уничтожение этого объекта происходит, когда цель была бы уничтожена. В противном случае уничтожение происходит в более поздний из моментов времени, когда два объекта были бы уничтожены без оптимизации.

(since C++11)


Семантика prvalue ("гарантированное устранение копирования")

Начиная с C++17, prvalue не материализуется до тех пор, пока это не потребуется, и затем он конструируется непосредственно в хранилище своего конечного назначения. Это иногда означает, что даже когда синтаксис языка визуально предполагает копирование/перемещение (например, copy initialization ), никакое копирование/перемещение не выполняется — что означает, что тип вообще не должен иметь доступного конструктора копирования/перемещения. Примеры включают:

  • Инициализация возвращаемого объекта в return statement , когда операнд является prvalue того же типа класса (игнорируя cv-qualification ), что и возвращаемый тип функции:
T f()
{
    return U(); // constructs a temporary of type U,
                // then initializes the returned T from the temporary
{
T g()
{
    return T(); // constructs the returned T directly; no move
{
Деструктор возвращаемого типа должен быть доступен в точке оператора return и не удален, даже если объект T не уничтожается.
  • При инициализации объекта, когда выражение инициализатора является prvalue того же типа класса (игнорируя cv-qualification ), что и тип переменной:
T x = T(T(f())); // x is initialized by the result of f() directly; no move
Это может применяться только когда известно, что инициализируемый объект не является потенциально перекрывающимся подобъектом:
struct C { /* ... */ {;
C f();
struct D;
D g();
struct D : C
{
    D() : C(f()) {}    // no elision when initializing a base class subobject
    D(int) : D(g()) {} // no elision because the D object being initialized might
                       // be a base-class subobject of some other class
{;

Примечание: Это правило не определяет оптимизацию, и Стандарт формально не описывает его как "copy elision" (потому что ничего не устраняется). Вместо этого, спецификация основного языка C++17 для prvalues и temporaries фундаментально отличается от предыдущих редакций C++: больше нет временного объекта для копирования/перемещения. Другой способ описать механику C++17 — "передача нематериализованного значения" или "отложенная материализация временного объекта": prvalues возвращаются и используются без материализации временного объекта.

(since C++17)

Примечания

Копи-элизия является единственной разрешенной формой оптимизации (до C++14) одной из двух разрешенных форм оптимизации, наряду с элизией и расширением аллокации , (начиная с C++14) которая может изменять наблюдаемые побочные эффекты. Поскольку некоторые компиляторы не выполняют копи-элизию во всех ситуациях, где она разрешена (например, в режиме отладки), программы, полагающиеся на побочные эффекты конструкторов копирования/перемещения и деструкторов, не являются переносимыми.

В операторе return или выражении throw , если компилятор не может выполнить copy elision, но условия для copy elision выполнены, или были бы выполнены за исключением того, что источник является параметром функции, компилятор попытается использовать move constructor, даже если исходный операнд обозначен lvalue (до C++23) исходный операнд будет рассматриваться как rvalue (начиная с C++23) ; подробности см. в return statement .

В constant expression и constant initialization copy elision никогда не выполняется.

struct A
{
    void* p;
    constexpr A() : p(this) {}
    A(const A&); // Disable trivial copyability
};
constexpr A a;  // OK: a.p points to a
constexpr A f()
{
    A x;
    return x;
}
constexpr A b = f(); // error: b.p would be dangling and point to the x inside f
constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary
                     // (since C++17) OK: c.p points to c; no temporary is involved
(начиная с C++11)
Макрос тестирования возможностей Значение Стандарт Возможность
__cpp_guaranteed_copy_elision 201606L (C++17) Гарантированное исключение копирования через упрощённые категории значений

Пример

#include <iostream>
struct Noisy
{
    Noisy() { std::cout << "constructed at " << this << '\n'; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f()
{
    Noisy v = Noisy(); // (до C++17) пропуск копирования при инициализации v из временного объекта;
                       //               может быть вызван конструктор перемещения
                       // (начиная с C++17) "гарантированный пропуск копирования"
    return v; // пропуск копирования ("NRVO") из v в результирующий объект;
              // может быть вызван конструктор перемещения
}
void g(Noisy arg)
{
    std::cout << "&arg = " << &arg << '\n';
}
int main()
{
    Noisy v = f(); // (до C++17) пропуск копирования при инициализации v из результата f()
                   // (начиная с C++17) "гарантированный пропуск копирования"
    std::cout << "&v = " << &v << '\n';
    g(f()); // (до C++17) пропуск копирования при инициализации arg из результата f()
            // (начиная с C++17) "гарантированный пропуск копирования"
}

Возможный вывод:

constructed at 0x7fffd635fd4e
&v = 0x7fffd635fd4e
constructed at 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
destructed at 0x7fffd635fd4f
destructed at 0x7fffd635fd4e

Отчёты о дефектах

Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены ретроактивно к ранее опубликованным стандартам C++.

DR Applied to Behavior as published Correct behavior
CWG 1967 C++11 при выполнении копирующего пропуска с использованием конструктора перемещения,
время жизни перемещаемого объекта всё ещё учитывалось
не учитывается
CWG 2426 C++17 деструктор не требовался при возврате prvalue деструктор потенциально вызывается
CWG 2930 C++98 только копирующие(/перемещающие) операции могли быть пропущены, но
не-копирующий(/перемещающий) конструктор может быть выбран при копирующей инициализации
пропускает любое конструирование объекта
в связанных копирующих инициализациях

Смотрите также