Copy elision
Когда выполняются определенные критерии, создание объекта класса из исходного объекта того же типа (игнорируя cv-квалификацию) может быть опущено, даже если выбранный конструктор и/или деструктор объекта имеют побочные эффекты. Это исключение создания объекта называется copy elision .
Содержание |
Объяснение
Копирование может быть опущено в следующих случаях (которые могут быть объединены для устранения нескольких копирований):
- В return statement функции с возвращаемым типом класса, когда операндом является имя не-volatile объекта obj с automatic storage duration (кроме параметра функции или параметра handler ), copy-initialization результирующего объекта может быть опущено путём непосредственного конструирования obj в результирующий объект вызова функции. Этот вариант copy elision известен как named return value optimization (NRVO).
|
(до C++17) |
|
(начиная с C++11) |
|
(начиная с C++20) |
Когда происходит копирующее исключение, реализация рассматривает источник и цель опущенной инициализации как просто два различных способа обращения к одному и тому же объекту.
|
Уничтожение происходит в более поздний из моментов времени, когда два объекта были бы уничтожены без оптимизации. |
(until C++11) |
|
Если первый параметр выбранного конструктора является rvalue-ссылкой на тип объекта, уничтожение этого объекта происходит, когда цель была бы уничтожена. В противном случае уничтожение происходит в более поздний из моментов времени, когда два объекта были бы уничтожены без оптимизации. |
(since C++11) |
Семантика prvalue ("гарантированное устранение копирования")Начиная с C++17, prvalue не материализуется до тех пор, пока это не потребуется, и затем он конструируется непосредственно в хранилище своего конечного назначения. Это иногда означает, что даже когда синтаксис языка визуально предполагает копирование/перемещение (например, copy initialization ), никакое копирование/перемещение не выполняется — что означает, что тип вообще не должен иметь доступного конструктора копирования/перемещения. Примеры включают:
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 {
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 |
только копирующие(/перемещающие) операции могли быть пропущены, но
не-копирующий(/перемещающий) конструктор может быть выбран при копирующей инициализации |
пропускает любое конструирование объекта
в связанных копирующих инициализациях |