(Я использую gcc с -O2
.)
Это походит на простую возможность игнорировать конструктора копии, так как нет никаких побочных эффектов к доступу к значению поля в a bar
копия a foo
; но конструктора копии вызывают, так как я получаю вывод meep meep!
.
#include <iostream>
struct foo {
foo(): a(5) { }
foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; }
int a;
};
struct bar {
foo F() const { return f; }
foo f;
};
int main()
{
bar b;
int a = b.F().a;
return 0;
}
Это не один из двух законных случаев элизии copy ctor, описанных в 12.8/15:
Оптимизация возвращаемого значения (когда автоматическая переменная возвращается из функции, и копирование этой автоматической переменной в возвращаемое значение устраняется путем построения автоматической переменной непосредственно в возвращаемом значении) - нет. f
не является автоматической переменной.
Временный инициализатор (когда временное значение копируется в объект, и вместо создания временного значения и его копирования, временное значение создается непосредственно в месте назначения) - нет f
тоже не временная. b.F()
- временное, но оно никуда не копируется, а просто происходит обращение к члену данных, так что к моменту выхода из F()
ускорять уже нечего.
Поскольку ни один из законных случаев элиминирования copy ctor не подходит, а копирование f
в возвращаемое значение F()
влияет на наблюдаемое поведение программы, стандарт запрещает его элиминировать. Если бы вы заменили печать на какую-то ненаблюдаемую деятельность и изучили сборку, вы могли бы увидеть, что этот конструктор копирования был оптимизирован. Но это было бы по правилу "as-if", а не по правилу исключения конструктора копий.
Конструктор копирования вызывается, потому что а) нет гарантии, что вы копируете значение поля без изменений, и б) потому что ваш конструктор копирования имеет побочный эффект (печатает сообщение).
Лучше думать об исключении копирования с точки зрения временного объекта. Так это описывается в стандарте. Временное может быть «свернуто» в постоянный объект, если оно скопировано в постоянный объект непосредственно перед его уничтожением.
Здесь вы создаете временный объект в функции return. На самом деле он ни в чем не участвует, поэтому вы хотите, чтобы его пропустили. Но что, если бы вы сделали
b.F().a = 5;
, если бы копия была опущена, и вы работали бы с исходным объектом, вы бы изменили b
через не-ссылку.
Удаление копии происходит только тогда, когда копия действительно не нужна. В частности, это когда есть один объект (назовите его A), который существует на время выполнения функции, и второй объект (назовите его B), который будет скопирован из первого объекта, и немедленно после этого A будет уничтожен (т.е. при выходе из функции).
В этом очень конкретном случае стандарт разрешает компилятору объединить A и B в два разных способа обращения к одному и тому же объекту. Вместо того, чтобы требовать, чтобы A был создан, затем B был скопирован из A, а затем A был уничтожен, он позволяет A и B рассматривать как два способа ссылки на один и тот же объект, поэтому (один) объект создается как A, и после того, как функция возвращается, называться B, но даже если конструктор копирования имеет побочные эффекты, копию, которая создает B из A, все равно можно пропустить. Также обратите внимание, что в этом случае A (как объект, отдельный от B) также никогда не уничтожается - например, если ваш dtor также имеет побочные эффекты, они также могут (будут) опущены.
Ваш код не соответствует этому шаблону - первый объект не перестает существовать сразу после того, как был использован для инициализации второго объекта.После возврата F ()
есть два экземпляра объекта. В этом случае оптимизация возвращаемого значения [Named] (также известная как copy elision) просто не применяется.
Демонстрационный код при применении исключения копирования:
#include <iostream>
struct foo {
foo(): a(5) { }
foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; }
int a;
};
int F() {
// RVO
std::cout << "F\n";
return foo();
}
int G() {
// NRVO
std::cout << "G\n";
foo x;
return x;
}
int main() {
foo a = F();
foo b = G();
return 0;
}
И MS VC ++, и g ++ оптимизируют оба оператора копирования из этого кода с включенной оптимизацией. g ++ оптимизирует и то, и другое, даже если оптимизация отключена. Когда оптимизация отключена, VC ++ оптимизирует анонимный возврат, но использует ctor копирования для именованного возврата.