2010-04-26 5 views
6

(я использую GCC с -O2.)Почему здесь не работает конструктор копирования?

Это кажется простой возможность Elide конструктора копирования, так как нет никаких побочных эффектов для доступа к значению поля в копии bar «s 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; 
} 
+0

Вы спрашиваете, почему вы создаете конструктор копирования, а не просто возвращаете значение поля исходного экземпляра? –

+1

Откуда вы знаете, что это не было исключено. И почему вы думаете, что это должно было быть? Измените свой вопрос в отношении этих двух запросов. – 2010-04-26 22:33:41

+0

@ Майкл, это правильно. –

ответ

11

Это ни один из двух юридических случаев копия CTOR элизии, описанных в 12.8/15:

оптимизации Возвращаемое значение (где автоматическая переменная возвращается из функции, а также копирования, что автоматическая для возвращаемого значения устраняется путем создания автоматического непосредственно в возвращаемом значении) - нет. f не является автоматической переменной.

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

Поскольку ни один из судебных дел о копировании копировальных ячеек элитной кожи, а копирование f на возвращаемое значение F() влияет на наблюдаемое поведение программы, стандарт запрещает его отмену. Если вы заменили печать на некоторую не наблюдаемую активность и рассмотрели сборку, вы можете увидеть, что этот конструктор копий был оптимизирован. Но это будет под правилом «как есть», а не под правилом элиминации экземпляра копии.

+1

Интересно, спасибо. Почему стандарт не разрешает этот случай? –

+0

По той же причине, что он не позволяет исключить любую старую функцию, которая содержит вызов 'std :: cout <<'. Большой вопрос: почему стандарт * всегда позволяет «оптимизациям» изменять наблюдаемое поведение программы? Ответ заключается в том, что было сочтено необходимым избегать смешных цепочек копирования времен и возвращать ценности, и никто не хотел бы полагаться на копирование, выполняемое в этих двух случаях. Если вы хотите избежать копирования в своем случае, вы можете вернуть 'const foo &'. В двух юридических случаях, связанных с копированием ctor elision, вы не можете избежать такой копии. –

+0

Итак, я подозреваю, что ни один случай элиции не был добавлен для вашего примера, это то, что вы можете сделать это сами, и при отсутствии необходимости, поведенческие специальные случаи плохие. Впрочем, это только предположение. –

1

Конструктор копирования называется потому, что а) нет никакой гарантии, вы копируете значение поля без изменения, и б) потому, что ваш конструктор копирования имеет побочный эффект (печатает сообщение).

+3

Копирование конструкторов с побочными эффектами может быть устранено. Итог - не писать такие конструкторы копирования. – 2010-04-26 22:47:39

1

Лучшим способом думать о копировании является вопрос о временном объекте. Так описывает стандарт. Временному разрешается «складывать» в постоянный объект, если он скопирован в постоянный объект непосредственно перед его уничтожением.

Здесь вы создаете временный объект в функции return. Он ни в чем не участвует, поэтому вы хотите, чтобы его пропустили. Но что, если вы сделали

b.F().a = 5; 

если копия была опущена, и вы оперировали исходный объект, вы бы изменили b через не ссылки.

+0

Но компилятор может получить только копию, если он не используется как lvalue. –

+0

@Jesse: в моем примере это не используется как lvalue. Он используется как левая сторона оператора '.'. – Potatoswatter

+0

@Potatoswatter - но тогда результат оператора '.' используется как lvalue. Я не на 100% уверен в своей терминологии на C++, поэтому, возможно, lvalue - это не правильное слово, но концепция, которую я ищу, должна быть транзитивной. –

2

Копирование эмиссии происходит только тогда, когда копия не нужна. В частности, это когда есть один объект (назовите его A), который существует для продолжительности выполнения функции, и второй объект (вызовите его B), который будет копироваться из первого объекта, и сразу после этого , A будет уничтожен (т.е. при выходе из функции).

В этом конкретном случае стандарт дает разрешение для компилятора объединить A и B в два отдельных способа обращения к одному и тому же объекту.Вместо того, чтобы требовать, чтобы A было создано, B - копия, построенная из A, а затем A уничтожается, она позволяет рассматривать A и B как два способа обращения к одному и тому же объекту, поэтому (один) объект создается как A, и после того, как возврат функции начинает упоминаться как B, но даже если конструктор копирования имеет побочные эффекты, копия, которая создает B из A, может быть пропущена. Также обратите внимание, что в этом случае A (как объект, отдельный от B) никогда не уничтожается - например, если ваш dtor также имел побочные эффекты, они могли бы (быть) также опущены.

Ваш код не соответствует этому шаблону - первый объект не перестает существовать сразу же после его использования для инициализации второго объекта. После того, как F() возвращает, есть два объекта экземпляра объекта. В этом случае Оптимизация возвращаемого значения [Именованная] (так называемая копия) просто не применяется.

Демо-код, когда будет применяться копия элизия:

#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++ и г ++ оптимизируют прочь и скопировать ctors из этого кода с включенной оптимизацией. g ++ оптимизирует оба варианта, даже если оптимизация отключена. Когда оптимизация отключена, VC++ оптимизирует анонимный доход, но использует копию ctor для именованного возврата.