Пропуск копирования
Опускает конструкторы копирования и перемещения (начиная с C++11), что приводит к семантике передачи по значению без копирования.
Объяснение
Семантика prvalue ("гарантированный пропуск копирования")Начиная с C++17, значение prvalue не материализуется до тех пор, пока оно не понадобится, а затем создаётся непосредственно в хранилище его конечного назначения. Иногда это означает, что даже когда синтаксис языка визуально предлагает копирование/перемещение (например, инициализация копированием), копирование/перемещение не выполняется, а это означает, что тип вообще не должен иметь доступного конструктора копирования/перемещения. Примеры включают:
T f()
{
return U(); // строит временный объект типа U,
// затем инициализирует возвращённый T из временного объекта
}
T g()
{
return T(); // строит возвращённый T напрямую; без перемещения
}
T x = T(T(f())); // x инициализируется результатом f() напрямую; без премещения
struct C { /* ... */ };
C f();
struct D;
D g();
struct D : C
{
D() : C(f()) {} // не пропускается при инициализации подобъекта базового класса
D(int) : D(g()) {} // не пропускается, потому что инициализируемый объект D может
// быть подобъектом базового класса какого-либо другого класса
};
Примечание. Это правило не определяет оптимизацию, и Стандарт формально не описывает её как "исключение копирования" (because nothing is being elided). Вместо этого спецификация prvalues и временных объектов базового языка C++17 фундаментально отличается от спецификации более ранних версий C++: больше нет временных значений для копирования/перемещения. Другой способ описать механику C++17 это "передача нематериализованного значения" или "отложенная временная материализация": значения prvalue возвращаются и используются без материализации временного значения. |
(начиная с C++17) |
Необязательное исключение копирования/перемещения (начиная с C++11)
При следующих обстоятельствах компиляторам разрешено, но не требуется пропускать создание копирования и перемещения (начиная с C++11) объектов класса, даже если конструктор копирования/перемещения (начиная с C++11) и деструктор имеют наблюдаемые побочные эффекты. Объекты создаются непосредственно в памяти, куда они в противном случае были бы скопированы/перемещены. Это оптимизация: даже когда она имеет место и конструктор копирования/перемещения (начиная с C++11) не вызывается, он всё равно должен присутствовать и быть доступным (как если бы оптимизации вообще не было), иначе программа будет некорректной:
- В операторе return, когда операнд является именем не-volatile объекта с автоматическим временем хранения, который не является параметром функции или параметром предложения catch и относится к тому же классовому типу (игнорируя cv-квалификацию) в качестве типа возвращаемого значения функции. Этот вариант пропуска копирования известен как NRVO, "оптимизация именованного возвращаемого значения - named return value optimization".
|
(до C++17) |
|
Оптимизация безымянного возвращаемого значения является обязательной и больше не рассматривается как форма пропуска копирования; смотрите выше. |
(начиная с C++17) |
|
(начиная с C++11) |
|
(начиная с C++20) |
Когда происходит пропуск копирования, реализация рассматривает источник и цель пропущенной операции копирования/перемещения (начиная с C++11) просто как два разных способа обращения к одному и тому же объекту, и уничтожение этого объекта происходит в более поздний момент времени, когда два объекта должны были бы быть уничтожены без оптимизации (за исключением того, что если параметр выбранного конструктора является ссылкой rvalue на тип объекта, уничтожение происходит, когда цель была бы уничтожена) (начиная с C++11).
Множественные пропуски копирования могут быть объединены в цепочку, чтобы исключить несколько копирований.
struct A
{
void *p;
constexpr A(): p(this) {}
};
constexpr A g()
{
A a;
return a;
}
constexpr A a; // a.p указывает на a
// constexpr A b = g(); // ошибка: b.p будет висячим и указывать на временное значение с
// автоматической длительностью хранения
void h()
{
A c = g(); // c.p может указывать на c или на эфемерное временное значение
}
extern const A d;
constexpr A f()
{
A e;
if (&e == &d)
return A();
else
return e;
// обязательное выполнение NRVO в контексте константной оценки приведёт к противоречию
// с тем, что NRVO выполняется тогда и только тогда, когда оно не выполняется
}
// constexpr A d = f(); // ошибка: d.p будет висячим
|
(начиная с C++11) |
Примечание
Исключение копирования это единственная разрешённая форма оптимизации (до C++14)это одна из двух разрешенных форм оптимизации, наряду с пропуском выделения памяти и расширением, (начиная с C++14) которая может изменить наблюдаемые побочные эффекты. Поскольку некоторые компиляторы не выполняют пропуск копирования во всех ситуациях, где это разрешено (например, в режиме отладки), программы, которые полагаются на побочные эффекты конструкторов и деструкторов копирования/перемещения, не переносимы.
|
В операторе return или выражении throw, если компилятор не может выполнить исключение копирования, но условия для исключения копирования выполнены или будут выполнены, за исключением того, что источник является параметром функции, компилятор попытается использовать конструктор перемещения, даже если исходный операнд обозначается lvalue (до C++23) исходный операнд будет рассматриваться как rvalue (начиная с C++23); подробности смотрите в оператор return. |
(начиная с C++11) |
| Макрос Тестирования функциональности | Значение | Стандарт | Функциональность |
|---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L |
(C++17) | Гарантированный пропуск копирования за счёт упрощения категорий значений |
Пример
#include <iostream>
struct Noisy
{
Noisy() { std::cout << "создан в " << this << '\n'; }
Noisy(const Noisy&) { std::cout << "создан копированием\n"; }
Noisy(Noisy&&) { std::cout << "создан перемещением\n"; }
~Noisy() { std::cout << "уничтожен в " << this << '\n'; }
};
Noisy f()
{
Noisy v = Noisy(); // пропуск копирования при инициализации v
// из временного объекта (до С++17) / prvalue (начиная с С++17)
return v; // NRVO из v в объект результата (не гарантируется даже в C++17),
} // если оптимизация отключена, вызывается конструктор перемещения
void g(Noisy arg)
{
std::cout << "&arg = " << &arg << '\n';
}
int main()
{
Noisy v = f(); // пропуск копирования при инициализации v
// из временного объекта, возвращаемого f() (до C++17)
// из prvalue f() (начиная с C++17)
std::cout << "&v = " << &v << '\n';
g(f()); // пропуск копирования при инициализации параметра функции g()
// из временного объекта, возвращаемого f() (до C++17)
// из prvalue f() (начиная с C++17)
}
Возможный вывод:
создан в 0x7fffd635fd4e
&v = 0x7fffd635fd4e
создан в 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
уничтожен в 0x7fffd635fd4f
уничтожен в 0x7fffd635fd4e
Отчёты о дефектах
Следующие изменения поведения были применены с обратной силой к ранее опубликованным стандартам C++:
| Номер | Применён | Поведение в стандарте | Корректное поведение |
|---|---|---|---|
| CWG 1967 | C++11 | когда пропуск копирования выполняется с помощью конструктора перемещения, время жизни перемещённого объекта по-прежнему учитывается |
не учитывается |
| CWG 2022 | C++11 | пропуск копирования был необязательным в константных выражениях |
пропуск копирования обязателен |
| CWG 2278 | C++11 | NRVO было обязательным в константных выражениях | NRVO запрещено в константных выражениях |
| CWG 2426 | C++17 | деструктор не требуется при возврате prvalue | деструктор потенциально вызывается |