Есть ли разница между инициализацией копирования и прямой инициализацией?


244

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

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());
}

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

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


1
И есть четвертый случай, обсуждаемый @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Дан Ниссенбаум

1
Просто примечание 2018 года: правила изменились в C ++ 17 , см., Например, здесь . Если мое понимание верно, в C ++ 17 оба утверждения фактически одинаковы (даже если ctor копирования явный). Кроме того, если бы выражение init было другого типа A, инициализация копирования не требовала бы наличия конструктора копирования / перемещения. Вот почему std::atomic<int> a = 1;в C ++ 17 нормально, но не раньше.
Даниэль Лангр

Ответы:


246

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

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

Это не то же самое. Первый default-initialized, если Aэто не POD, и не выполняет никакой инициализации для POD (Прочтите 8.6 / 9 ). Вторая копия инициализирует: Value-инициализирует временное и затем копирует это значение в c2(Прочтите 5.2.3 / 2 и 8.6 / 14 ). Это, конечно, потребует неявного конструктора копирования (см. 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аргументом. Победителем является функция преобразования: поскольку если у нас есть две функции-кандидата, которые принимают ссылку на один и тот же тип, выигрывает менее константная версия (кстати, это также механизм, который предпочитает вызовы неконстантных функций-членов для объекты).

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

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


Вот это да. Я даже не догадывался об объявлении функции. Я в значительной степени должен принять ваш ответ только за то, что я единственный, кто знает об этом. Есть ли причина, по которой объявления функций работают таким образом? Было бы лучше, если бы c3 обрабатывался по-другому внутри функции.
rlbond

4
Бах, извините, ребята, но мне пришлось удалить свой комментарий и опубликовать его снова из-за нового движка форматирования: потому что в параметрах функции R() == R(*)()и T[] == T*. То есть типы функций являются типами указателей на функции, а типы массивов являются типами указателей на элементы. Это отстой. Это может быть A c3((A()));обойдено (parens вокруг выражения).
Йоханнес Шауб -

4
Могу я спросить, что означает «Читать 8.5 / 14»? К чему это относится? Книга? Глава? Сайт?
АзП

9
@AzP Многие люди на SO часто хотят ссылки на спецификацию C ++, и именно это я и сделал здесь в ответ на запрос rlbond «Пожалуйста, приведите текст в качестве доказательства». Я не хочу ссылаться на спецификацию, так как это раздувает мой ответ и требует гораздо больше работы, чтобы идти в ногу со временем (избыточность).
Йоханнес Шауб - лит

1
@luca Я рекомендую начать новый вопрос для этого, чтобы другие могли получить пользу от ответа, который дают люди
Йоханнес Шауб - Lit

49

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

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

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;
}

2
Полезная ссылка: «Язык программирования C ++, специальное издание» Бьярна Страуструпа, раздел 10.4.4.1 (стр. 245). Описывает инициализацию копирования и назначение копии и почему они принципиально отличаются (хотя они оба используют оператор = в качестве синтаксиса).
Нааф

Незначительная гнида, но мне действительно не нравится, когда люди говорят, что «A a (x)» и «A a = x» равны. Строго говоря, нет. Во многих случаях они будут делать одно и то же, но можно создать примеры, в которых в зависимости от аргумента фактически вызываются разные конструкторы.
Ричард Корден

Я не говорю о "синтаксической эквивалентности". Семантически оба способа инициализации одинаковы.
Мехрдад Афшари

@MehrdadAfshari В коде ответа Йоханнеса вы получаете различный вывод в зависимости от того, какой из двух вы используете.
Брайан Гордон

1
@BrianGordon Да, ты прав. Они не эквивалентны. Я обратился к комментарию Ричарда в моей редакции давно.
Мехрдад Афшари

22

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;
}

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


5
+1. Хороший ответ. Хорошо также отметить явную версию. Кстати, важно отметить, что вы не можете иметь обе версии одной перегрузки конструктора одновременно. Таким образом, он просто не сможет скомпилироваться в явном случае. Если они оба компилируются, они должны вести себя одинаково.
Мехрдад Афшари

4

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

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

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


4

Отметить:

[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 ++.


4

Вы можете увидеть его разницу в типах конструктора 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

3

Отвечая по отношению к этой части:

A c2 = A (); С3 (А ());

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

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

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


Ни в одном из примеров "список выражений не указывает более одного значения". Какое это имеет отношение?
underscore_d

0

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

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

A a = 5;
A a(5);

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

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

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

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


Это не оптимизация . Компилятор должен вызывать конструктор одинаково в обоих случаях. В результате ни один из них не скомпилируется, если у вас просто есть operator =(const int)и нет A(const int). Смотрите ответ @ jia3ep для более подробной информации.
Мехрдад Афшари

Я верю, что ты на самом деле прав. Однако он будет прекрасно компилироваться с использованием конструктора копирования по умолчанию.
Дборба

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

0

Это из языка программирования C ++ Бьярна Страуструпа:

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.