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

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

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

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

В C++ 17 значение 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());

Это не то же самое. Первая инициализация по умолчанию инициализируется, если 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 выдает правильное предупреждение о неоднозначности.

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

Ответ 2

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

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

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

Ответ 3

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

Примечание:

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

I.e., для инициализации копирования.

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

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

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

Технический голос: [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.

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

Ответ 5

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

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

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

Ответ 6

Отвечая на эту часть:

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

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

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

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

Ответ 7

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

Ответ 8

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

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

A a = 5;
A a(5);

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

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

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

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

Ответ 9

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

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