Есть ли разница между инициализацией копирования и прямой инициализацией?
Предположим, у меня есть эта функция:
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 ответов
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)
просто "проходит" через результирующий объект внешнего значения, чтобы быть также результирующим объектом внутреннего значения.
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).
Копание в инициализации Прямая и копирование инициализации
Хотя они выглядят одинаково и должны делать то же самое, в некоторых случаях эти две формы заметно отличаются. Две формы инициализации - прямая и копируемая инициализация:
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>
Как это работает и почему выводит этот результат?
Прямая инициализация
Сначала он ничего не знает о преобразовании. Он просто попытается вызвать конструктор. В этом случае следующий конструктор доступен и является точным соответствием:
B(A const&)
Для вызова этого конструктора не требуется никакого преобразования, а тем более определенного пользователем преобразования (обратите внимание, что здесь также не происходит преобразования квалификации const). И поэтому прямая инициализация будет называть это.
Копировать инициализацию
Как сказано выше, инициализация копирования создаст последовательность преобразования, когда
a
не имеет типаB
или производные от него (что явно имеет место здесь). Так что он будет искать способы сделать преобразование и найдет следующих кандидатовB(A const&) operator B(A&);
Обратите внимание, как я переписал функцию преобразования: тип параметра отражает тип
this
указатель, который в неконстантной функции-члене является неконстантным. Теперь мы называем этих кандидатов сx
в качестве аргумента. Победителем является функция преобразования: поскольку, если у нас есть две функции-кандидата, которые принимают ссылку на один и тот же тип, выигрывает менее константная версия (кстати, это также механизм, который предпочитает вызовы неконстантных функций-членов для объекты).Обратите внимание, что если мы изменим функцию преобразования на функцию-член const, то преобразование будет неоднозначным (поскольку оба имеют тип параметра
A const&
затем): компилятор Comeau отклоняет его должным образом, но GCC принимает его в непедантическом режиме. Переключение на-pedantic
тем не менее, выдает правильное предупреждение о неоднозначности.
Надеюсь, это поможет понять, как эти две формы различаются!
Назначение отличается от инициализации.
Обе следующие строки выполняют инициализацию. Один вызов конструктора выполняется:
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;
}
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;
}
Если в вашем классе нет явных конструкторов, то явные и неявные вызовы идентичны.
Вы можете увидеть его разницу в 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
Отметить:
[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++.
Отвечая по отношению к этой части:
A c2 = A (); С3(А ());
Поскольку большинство ответов до с ++11, я добавляю, что C++11 должен сказать по этому поводу:
Спецификатор простого типа (7.1.6.2) или спецификатор typename (14.6), за которым следует список выражений в скобках, создает значение указанного типа по заданному списку выражений. Если список выражений является одним выражением, выражение преобразования типа эквивалентно (в определенности и если определено в значении) соответствующему приведенному выражению (5.4). Если указанный тип является типом класса, тип класса должен быть завершен. Если в списке выражений указано более одного значения, тип должен быть классом с соответствующим образом объявленным конструктором (8.5, 12.1), а выражение T(x1, x2, ...) фактически эквивалентно объявлению T t(х1, х2, ...); для некоторой изобретенной временной переменной t, результатом которой является значение t в качестве значения.
Так что оптимизация или нет они эквивалентны по стандарту. Обратите внимание, что это соответствует тому, что упоминали другие ответы. Просто процитирую то, что стандарт говорит ради правильности.
Первая группировка: это зависит от того, что A_factory_func
возвращается. Первая строка является примером инициализации копии, вторая строка - прямой инициализацией. Если A_factory_func
возвращает A
объект, то они эквивалентны, они оба вызывают конструктор копирования для A
в противном случае первая версия создает значение типа A
из доступных операторов преобразования для типа возврата A_factory_func
или соответствующий A
конструкторы, а затем вызывает конструктор копирования для создания a1
из этого временного. Вторая версия пытается найти подходящий конструктор, который принимает все A_factory_func
возвращает, или что-то принимает, что возвращаемое значение может быть неявно преобразовано в.
Вторая группа: точно такая же логика, за исключением того, что встроенные типы не имеют каких-либо экзотических конструкторов, поэтому на практике они идентичны.
Третья группа: c1
инициализируется по умолчанию, c2
инициализируется копией из значения, инициализированного временно. Любые члены c1
которые имеют тип pod (или члены-члены и т. д. и т. д.), могут не инициализироваться, если предоставленные пользователем конструкторы по умолчанию (если таковые имеются) не инициализируют их явно. За c2
, это зависит от того, существует ли предоставленный пользователем конструктор копирования и будет ли он соответствующим образом инициализировать эти элементы, но все члены временных будут инициализированы (инициализируются нулями, если не было явно инициализировано иным образом). Как горит, c3
это ловушка Это на самом деле объявление функции.
Это из языка программирования C++ Бьярна Страуструпа:
Инициализация со знаком = считается инициализацией копии. В принципе, копия инициализатора (объекта, из которого мы копируем) помещается в инициализированный объект. Однако такая копия может быть оптимизирована (исключена), и операция перемещения (на основе семантики перемещения) может использоваться, если инициализатором является rvalue. Отсутствие = делает инициализацию явной. Явная инициализация известна как прямая инициализация.
Многие из этих случаев зависят от реализации объекта, поэтому сложно дать вам конкретный ответ.
Рассмотрим случай
A a = 5;
A a(5);
В этом случае при условии правильного оператора присваивания и инициализирующего конструктора, которые принимают один целочисленный аргумент, способ реализации указанных методов влияет на поведение каждой строки. Однако для одного из них является обычной практикой вызывать другое в реализации, чтобы исключить дублирование кода (хотя в таком простом случае реальной цели не было бы).
Редактировать: Как уже упоминалось в других ответах, первая строка на самом деле будет вызывать конструктор копирования. Рассматривайте комментарии, относящиеся к оператору присваивания, как поведение, относящееся к отдельному назначению.
Тем не менее, то, как компилятор оптимизирует код, будет иметь свое влияние. Если у меня есть инициализирующий конструктор, вызывающий оператор "=" - если компилятор не делает оптимизаций, верхняя строка будет выполнять 2 перехода, а не один в нижней строке.
Теперь для наиболее распространенных ситуаций ваш компилятор оптимизирует эти случаи и устранит этот тип неэффективности. Таким образом, все описанные вами ситуации окажутся одинаковыми. Если вы хотите точно увидеть, что делается, вы можете посмотреть на объектный код или вывод сборки вашего компилятора.