Есть ли разница между инициализацией копирования и прямой инициализацией? - PullRequest
221 голосов
/ 27 июня 2009

Предположим, у меня есть эта функция:

void my_test()
{
    A a1 = A_factory_func();
    A a2(A_factory_func());

    double b1 = 0.5;
    double b2(0.5);

    A c1;
    A c2 = A();
    A c3(A());
}

Являются ли эти утверждения в каждой группе идентичными? Или есть дополнительная (возможно, оптимизируемая) копия в некоторых инициализациях?

Я видел, как люди говорили обе вещи. Пожалуйста, цитируйте текст в качестве доказательства. Также добавьте другие случаи, пожалуйста.

Ответы [ 8 ]

235 голосов
/ 27 июня 2009

C ++ 17 Обновление

В C ++ 17 значение A_factory_func() изменилось с создания временного объекта (C ++ <= 14) на простое указание инициализации любого объекта, которому это выражение инициализируется (условно говоря) в C ++ 17. Эти объекты (называемые «объектами результата») являются переменными, созданными объявлением (например, <code>a1), искусственными объектами, созданными, когда инициализация заканчивается, отбрасываются или если объект необходим для привязки ссылки (например, в A_factory_func();) В последнем случае объект создается искусственно, что называется «временной материализацией», потому что A_factory_func() не имеет переменной или ссылки, которая в противном случае потребовала бы существования объекта).

В качестве примеров в нашем случае, в случае специальных правил a1 и a2 говорится, что в таких объявлениях результирующий объект инициализатора prvalue того же типа, что и a1, является переменной a1, и следовательно, A_factory_func() непосредственно инициализирует объект a1. Любое промежуточное приведение функционального стиля не будет иметь никакого эффекта, потому что A_factory_func(another-prvalue) просто «проходит» через объект результата внешнего значения prvalue, чтобы быть также объектом результата внутреннего значения prvalue.


A a1 = A_factory_func();
A a2(A_factory_func());

Зависит от того, какой тип A_factory_func() возвращает. Я предполагаю, что он возвращает A - тогда он делает то же самое - за исключением того, что когда конструктор копирования является явным, тогда первый потерпит неудачу. Читать 8,6 / 14

double b1 = 0.5;
double b2(0.5);

Это то же самое, потому что это встроенный тип (здесь это не тип класса). Читайте 8,6 / 14 .

A c1;
A c2 = A();
A c3(A());

Это не то же самое. Первое значение по умолчанию инициализируется, если A не является POD, и не выполняет никакой инициализации для POD (Чтение 8.6 / 9 ). Вторая копия инициализирует: Значение - инициализирует временное значение, а затем копирует это значение в c2 (чтение 5.2.3 / 2 и 8.6 / 14 ). Это, конечно, потребует неявного конструктора копирования (Read 8.6 / 14 и 12.3.1 / 3 и 13.3.1.3 / 1 ). Третий создает объявление функции для функции c3, которая возвращает A и принимает указатель на функцию, возвращающую A (Чтение 8.2 ).


Копирование в инициализации Прямая и копирование инициализации

Хотя они выглядят одинаково и должны делать то же самое, в некоторых случаях эти две формы заметно отличаются. Две формы инициализации - прямая и копируемая инициализация:

T t(x);
T t = x;

Существует поведение, которое мы можем отнести к каждому из них:

  • Прямая инициализация ведет себя как вызов функции перегруженной функции: функции, в этом случае, являются конструкторами T (включая explicit единиц), а аргумент равен x. Разрешение перегрузки найдет наилучшего подходящего конструктора и при необходимости выполнит любое неявное преобразование.
  • При инициализации копирования создается неявная последовательность преобразования: она пытается преобразовать x в объект типа T. (Затем он может скопировать этот объект в инициализируемый объект, поэтому также необходим конструктор копирования - но это не важно ниже)

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

Я очень старался и получил следующий код для вывода различного текста для каждой из этих форм , без использования конструкторов "очевидный" - explicit.

#include <iostream>
struct B;
struct A { 
  operator B();
};

struct B { 
  B() { }
  B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() { 
  A a;
  B b1(a);  // 1)
  B b2 = a; // 2)
}
// output: <direct> <copy>

Как это работает и почему выводит этот результат?

  1. Прямая инициализация

    Сначала он ничего не знает о преобразовании. Он просто попытается вызвать конструктор. В этом случае доступен следующий конструктор с точным соответствием :

    B(A const&)
    

    Нет преобразования, тем более определяемого пользователем преобразования, необходимого для вызова этого конструктора (обратите внимание, что здесь также не происходит преобразования квалификации const). И поэтому прямая инициализация будет называть это.

  2. Копировать инициализацию

    Как сказано выше, при инициализации копирования будет создана последовательность преобразования, когда a не имеет типа B или не является производным от него (что, безусловно, имеет место в данном случае). Поэтому он будет искать способы сделать преобразование и найдет следующих кандидатов

    B(A const&)
    operator B(A&);
    

    Обратите внимание, как я переписал функцию преобразования: тип параметра отражает тип указателя this, который в неконстантной функции-члене является неконстантным. Теперь мы называем этих кандидатов с x в качестве аргумента. Победителем является функция преобразования: поскольку если у нас есть две функции-кандидата, принимающие ссылку на один и тот же тип, то выигрывает версия less const (кстати, это также механизм, который предпочитает не вызов функции-члена const для неконстантных объектов).

    Обратите внимание, что если мы изменим функцию преобразования на функцию-член const, то преобразование будет неоднозначным (так как тогда оба имеют тип параметра A const&): компилятор Comeau отклоняет его должным образом, но GCC принимает его не -педантический режим. Однако переключение на -pedantic также приводит к правильному предупреждению о неоднозначности.

Надеюсь, это поможет понять, как эти две формы различаются!

45 голосов
/ 27 июня 2009

Назначение отличается от инициализация .

Обе следующие строки делают инициализацию . Выполнен один вызов конструктора:

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

но это не эквивалентно:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

У меня нет текста, чтобы доказать это, но экспериментировать очень просто:

#include <iostream>
using namespace std;

class A {
public:
    A() { 
        cout << "default constructor" << endl;
    }

    A(const A& x) { 
        cout << "copy constructor" << endl;
    }

    const A& operator = (const A& x) {
        cout << "operator =" << endl;
        return *this;
    }
};

int main() {
    A a;       // default constructor
    A b(a);    // copy constructor
    A c = a;   // copy constructor
    c = b;     // operator =
    return 0;
}
16 голосов
/ 27 июня 2009

double b1 = 0.5; - это неявный вызов конструктора.

double b2(0.5); явный вызов.

Посмотрите на следующий код, чтобы увидеть разницу:

#include <iostream>
class sss { 
public: 
  explicit sss( int ) 
  { 
    std::cout << "int" << std::endl;
  };
  sss( double ) 
  {
    std::cout << "double" << std::endl;
  };
};

int main() 
{ 
  sss ddd( 7 ); // calls int constructor 
  sss xxx = 7;  // calls double constructor 
  return 0;
}

Если в вашем классе нет явных конструкторов, то явные и неявные вызовы идентичны.

4 голосов
/ 08 октября 2011

Примечание:

[12,2 / 1] Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

Т.е., для инициализации копирования.

[12,8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

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

Другими словами, инициализация копирования аналогична прямой инициализации в большинстве случаев , где написан понятный код. Поскольку прямая инициализация потенциально вызывает произвольные (и, следовательно, вероятно, неизвестные) преобразования, я предпочитаю всегда использовать копирование-инициализацию, когда это возможно. (С бонусом, который на самом деле выглядит как инициализация.)

Технический статус: [12.2 / 1 продолжение сверху] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

Рад, что я не пишу компилятор C ++.

3 голосов
/ 18 мая 2015

Относительно этой части:

A c2 = A (); A c3 (A ());

Поскольку большинство ответов до c ++ 11, я добавляю, что c ++ 11 должен сказать по этому поводу:

Спецификатор простого типа (7.1.6.2) или спецификатор типа (14.6) сопровождаемый заключенным в скобки выражение-список создает значение указанный тип с учетом списка выражений. Если список выражений одно выражение, выражение преобразования типа эквивалентно (в определенность, и если определено в значении) к соответствующему составу выражение (5.4). Если указанный тип является типом класса, класс тип должен быть полным. Если в списке выражений указано больше одиночное значение, тип должен быть классом с соответствующим конструктор (8.5, 12.1), а выражение T (x1, x2, ...) по сути эквивалентно объявлению T t (x1, x2, ...); для некоторых изобрел временную переменную t, результатом которой является значение t как prvalue.

Так что оптимизация или нет, они эквивалентны по стандарту. Обратите внимание, что это соответствует тому, что упоминали другие ответы. Просто процитирую то, что стандарт говорит ради правильности.

3 голосов
/ 27 июня 2009

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

Вторая группировка: точно такая же логика сохраняется, за исключением того, что встроенные типы не имеют каких-либо экзотических конструкторов, поэтому на практике они идентичны.

Третья группировка: c1 инициализируется по умолчанию, c2 инициализируется копированием из значения, инициализированного временно. Любые члены c1, имеющие тип pod (или члены-члены и т. Д. И т. Д.), Не могут быть инициализированы, если пользователь предоставил конструкторы по умолчанию (если таковые имеются), которые не инициализируют их явно. Для c2 это зависит от того, существует ли предоставленный пользователем конструктор копирования и будет ли он соответствующим образом инициализировать эти элементы, но все члены временных будут инициализированы (инициализируются нулями, если явно не инициализированы иным образом). Как замечено, c3 - ловушка. На самом деле это объявление функции.

0 голосов
/ 16 июня 2018

Вы можете увидеть разницу между типами конструкторов explicit и implicit при инициализации объекта:

Классы:

class A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
};

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

А в функции main :

int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast

//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
}

По умолчанию конструктор имеет вид implicit, поэтому у вас есть два способа его инициализации:

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization

И, определяя структуру как explicit, у вас есть только один прямой путь:

B b2(2);        // this is direct initialization
B b5 = (B)1;    // not problem if you either use of assign to initialize and cast it as static_cast
0 голосов
/ 27 июня 2009

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

Рассмотрим случай

A a = 5;
A a(5);

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

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

Тем не менее, то, как компилятор оптимизирует код, будет иметь свое влияние. Если у меня есть инициализирующий конструктор, вызывающий оператор "=" - если компилятор не выполняет оптимизацию, тогда верхняя строка будет выполнять 2 перехода, а не один в нижней строке.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...