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

214

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

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

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

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

И есть четвертый случай, обсуждаемый @JohannesSchaub -A c1; A c2 = c1; A c3(c1);. Dan Nissenbaum
Just a 2018 note: Правила изменились вC++17см., например,here, Если мое понимание верно, в C ++ 17 оба утверждения фактически одинаковы (даже если ctor копирования явный). Более того, если выражение init будет другого типа, чемAдля инициализации копирования не требуется наличие конструктора копирования / перемещения. Вот почемуstd::atomic<int> a = 1; в C ++ 17 нормально, но не раньше. Daniel Langr

Ваш Ответ

8   ответов
1

поэтому сложно дать вам конкретный ответ.

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

A a = 5;
A a(5);

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

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

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

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

Я верю, что вы на самом деле правы. Однако он будет прекрасно компилироваться с использованием конструктора копирования по умолчанию.
Кроме того, как я уже упоминал, обычной практикой является использование в конструкторе копирования вызова оператора присваивания, после чего оптимизация компилятора вступает в игру.
Это неoptimization, Компиляторhas to вызовите конструктор одинаково в обоих случаях. В результате ни один из них не скомпилируется, если вы простоoperator =(const int) и нетA(const int), См. Ответ @ jia3ep для получения более подробной информации.
5

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

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

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

Техническая goriness: [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

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

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

A simple-type-specifier (7.1.6.2) or typename-specifier (14.6) followed by a parenthesized expression-list constructs a value of the specified type given the expression list. If the expression list is a single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4). If the type specified is a class type, the class type shall be complete. If the expression list specifies more than a single value, the type shall be a class with a suitably declared constructor (8.5, 12.1), and the expression T(x1, x2, ...) is equivalent in effect to the declaration T t(x1, x2, ...); for some invented temporary variable t, with the result being the value of t as a prvalue.

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

Ни один из ваших примеров & apos; & quot; список выражений указывает более одного значения & quot ;. Какое это имеет отношение?
15

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

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

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

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

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

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

1

explicit а такжеimplicit Типы конструктора при инициализации объекта:

Classes :

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

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

And in the main function :

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
44

Assignment отличается отinitialization.

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

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;
}
@MehrdadAfshari В коде ответа Йоханнеса вы получаете различный вывод в зависимости от того, какой из двух вы используете.
Незначительная гнида, но мне действительно не нравится, когда люди говорят, что "A a (x)" и "A a = x"; равны. Строго говоря, нет. Во многих случаях они будут делать одно и то же, но можно создать примеры, в которых в зависимости от аргумента фактически вызываются разные конструкторы.
Я не говорю о "синтаксической эквивалентности". Семантически оба способаinitialization подобные.
@BrianGordon Да, вы правы. Они не эквивалентны. Я обратился к комментарию Ричарда в моей редакции давно.
Полезная ссылка: & quot; Язык программирования C ++, специальная версия & quot; Бьярном Страуструпом, раздел 10.4.4.1 (стр. 245). Описывает инициализацию копирования и назначение копии и почему они принципиально отличаются (хотя они оба используют оператор = в качестве синтаксиса).
232
C++17 Update

A_factory_func() изменено с создания временного объекта (C ++ & lt; = 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-инициализируется, если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).

Delving into Initializations Прямая и копирование инициализации

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

T t(x);
T t = x;

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

Direct initialization behaves like a function call to an overloaded function: The functions, in this case, are the constructors of T (including explicit ones), and the argument is x. Overload resolution will find the best matching constructor, and when needed will do any implicit conversion required. Copy initialization constructs an implicit conversion sequence: It tries to convert x to an object of type T. (It then may copy over that object into the to-initialized object, so a copy constructor is needed too - but this is not important below)

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

Я старался иполучил следующий код для вывода различного текста для каждой из этих формбез использования & quot; очевидного & quot; через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>

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

Direct initialization

It first doesn't know anything about conversion. It will just try to call a constructor. In this case, the following constructor is available and is an exact match:

B(A const&)

There is no conversion, much less a user defined conversion, needed to call that constructor (note that no const qualification conversion happens here either). And so direct initialization will call it.

Copy initialization

As said above, copy initialization will construct a conversion sequence when a has not type B or derived from it (which is clearly the case here). So it will look for ways to do the conversion, and will find the following candidates

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

Notice how I rewrote the conversion function: The parameter type reflects the type of the this pointer, which in a non-const member function is to non-const. Now, we call these candidates with x as argument. The winner is the conversion function: Because if we have two candidate functions both accepting a reference to the same type, then the less const version wins (this is, by the way, also the mechanism that prefers non-const member function calls for non-const objects).

Note that if we change the conversion function to be a const member function, then the conversion is ambiguous (because both have a parameter type of A const& then): The Comeau compiler rejects it properly, but GCC accepts it in non-pedantic mode. Switching to -pedantic makes it output the proper ambiguity warning too, though.

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

Вот это да. Я даже не догадывался об объявлении функции. Я в значительной степени должен принять ваш ответ только за то, что я единственный, кто знает об этом. Есть ли причина, по которой объявления функций работают таким образом? Было бы лучше, если бы c3 обрабатывался по-другому внутри функции. rlbond
@AzP Многие люди в SO часто хотят ссылки на спецификацию C ++, и это то, что я сделал здесь, в ответ на запрос rlbond'а. Пожалуйста, приведите текст в качестве доказательства. Я не хочу ссылаться на спецификацию, так как это раздувает мой ответ и требует гораздо больше работы для поддержания актуальности (избыточность).
Бах, извините, ребята, но мне пришлось удалить свой комментарий и опубликовать его снова из-за нового движка форматирования: потому что в параметрах функции,R() == R(*)() а такжеT[] == T* , То есть типы функций являются типами указателей на функции, а типы массивов являются типами указателей на элементы. Это отстой. Это можно обойтиA c3((A())); (Парень вокруг выражения).
Я хочу поделиться этимrelated link
Могу я спросить, что "прочитано в 8.5 / 14"? средства? К чему это относится? Книга? Глава? Сайт?

Похожие вопросы