Чи є різниця між ініціалізацією копії та прямою ініціалізацією?


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);.
Dan Nissenbaum

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спеціальних правил говориться, що в таких деклараціях об'єкт результату ініціалізатора первинного значення того ж типу, що 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());

Це не те саме. Перший за замовчуванням ініціалізується, якщо 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 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вказівника, який у функції non-const-члена є не-const. Тепер ми називаємо цих кандидатів xаргументом. Переможець - це функція перетворення: Оскільки, якщо у нас є дві функції-кандидати, обидві приймають посилання на один і той же тип, то виграє менше версія const (це, до речі, також механізм, який віддає перевагу функції, що не має статусу члена, вимагає не -об'єкти).

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

Я сподіваюся, що це дещо допомагає зрозуміти, чим відрізняються ці дві форми!


Ого. Я навіть не розумів про декларацію функції. Мені доводиться приймати вашу відповідь лише за те, що я єдиний про це знав. Чи є причина, що декларації функції працюють таким чином? Було б краще, якби c3 поводилися по-різному всередині функції.
rlbond

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

4
Чи можу я запитати, що означає "Прочитати 8.5 / 14"? Що це стосується? Книга? Глава? Веб-сайт?
AzP

9
@AzP багато людей на SO часто хочуть посилань на специфікацію C ++, і саме це я зробив тут, відповідаючи на запит rlbond "Будь ласка, цитуйте текст як доказ". Я не хочу цитувати специфікацію, тому що це полегшує мою відповідь і є набагато більше роботи, щоб бути в курсі (надмірність).
Йоханнес Шауб - ліб

1
@luca я рекомендую почати нове запитання щодо цього, щоб інші могли отримати користь від відповіді, яку люди дають також
Йоханнес Шауб - ліб

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 ++, спеціальне видання" Bjarne Stroustrup, розділ 10.4.4.1 (стор. 245). Описує ініціалізацію копіювання та призначення копії та чому вони принципово відрізняються (хоча обидва вони використовують оператор = як синтаксис).
Naaff

Незначна нітка, але мені дуже не подобається, коли люди кажуть, що "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 ...

Іншими словами, хороший компілятор не створить копію для ініціалізації копіювання, коли її можна буде уникнути; натомість він просто викличе конструктор безпосередньо - тобто, як і для прямої ініціалізації.

Іншими словами, ініціалізація копіювання подібна до прямої ініціалізації в більшості випадків <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.

Радий, що не пишу компілятор на 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 (); A c3 (A ());

Оскільки більшість відповідей є попередніми c ++ 11, я додаю, що c ++ 11 має сказати з цього приводу:

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

Тож оптимізація чи ні вони еквівалентні, як за стандартом. Зауважте, що це відповідає тому, що згадували інші відповіді. Просто цитуйте, що стандарт повинен сказати заради коректності.


Жоден із ваших прикладів "" список виразів не вказує більше одного значення ". Наскільки це стосується цього?
підкреслюй_d

0

Багато цих випадків підлягають реалізації об'єкта, тому важко дати вам конкретну відповідь.

Розглянемо випадок

A a = 5;
A a(5);

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

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

Однак, як компілятор оптимізує код, це матиме власний вплив. Якщо у мене конструктор ініціалізації викликає оператора "=" - якщо компілятор не робить оптимізацій, то в верхньому рядку буде виконуватися 2 стрибки на відміну від одного в нижньому рядку.

Тепер для найпоширеніших ситуацій ваш компілятор буде оптимізувати ці випадки та усунути цей тип неефективності. Тому ефективно всі різні описувані ситуації вийдуть однаковими. Якщо ви хочете побачити, що саме робиться, ви можете подивитися об'єктний код або збірний висновок вашого компілятора.


Це не оптимізація . У обох випадках компілятор повинен викликати конструктор. Як результат, жоден з них не складеться, якщо у вас просто є operator =(const int)і ні A(const int). Дивіться відповідь @ jia3ep для отримання більш детальної інформації.
Мехрдад Афшарі

Я вірю, що ти правдивий. Однак вона складеться чудово за допомогою конструктора копій за замовчуванням.
dborba

Крім того, як я вже згадував, звичайною практикою є конструктор копій викликати оператора призначення, коли оптимізація компілятора починає грати
dborba

0

Це з мови програмування на C ++ від Bjarne Stroustrup:

Ініціалізація з а = вважається ініціалізацією копії . В принципі, копія ініціалізатора (об'єкта, з якого ми копіюємо) розміщується в ініціалізований об’єкт. Однак така копія може бути оптимізована (відхилена), і операція переміщення (заснована на семантиці переміщення) може бути використана, якщо ініціалізатор є реальним значенням. Якщо вийти з поля =, робить ініціалізацію явною. Явна ініціалізація відома як пряма ініціалізація .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.