Apakah ada perbedaan antara inisialisasi salin dan inisialisasi langsung?


244

Misalkan saya memiliki fungsi ini:

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

Dalam setiap pengelompokan, apakah pernyataan ini identik? Atau adakah salinan tambahan (mungkin dapat dioptimalkan) di beberapa inisialisasi?

Saya telah melihat orang-orang mengatakan kedua hal itu. Silakan mengutip teks sebagai bukti. Tolong tambahkan juga case lain.


1
Dan ada kasus keempat yang dibahas oleh @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum

1
Hanya catatan 2018: Aturan telah berubah di C ++ 17 , lihat, misalnya, di sini . Jika pemahaman saya benar, dalam C ++ 17, kedua pernyataan secara efektif sama (bahkan jika copy ctor eksplisit). Selain itu, jika ekspresi init akan menjadi tipe selain A, inisialisasi salinan tidak memerlukan keberadaan copy / move constuctor. Inilah sebabnya mengapa std::atomic<int> a = 1;ok di C ++ 17 tetapi tidak sebelumnya.
Daniel Langr

Jawaban:


246

Pembaruan C ++ 17

Dalam C ++ 17, makna A_factory_func()berubah dari menciptakan objek sementara (C ++ <= 14) menjadi hanya menentukan inisialisasi objek apa pun yang menjadi inisialisasi ekspresi ini (dalam bahasa longgar) dalam C ++ 17. Objek-objek ini (disebut "objek hasil") adalah variabel yang dibuat oleh deklarasi (seperti a1), objek buatan yang dibuat ketika inisialisasi berakhir dibuang, atau jika suatu objek diperlukan untuk pengikatan referensi (seperti, dalam A_factory_func();. Dalam kasus terakhir, sebuah objek dibuat secara artifisial, yang disebut "perwujudan sementara", karena A_factory_func()tidak memiliki variabel atau referensi yang sebaliknya membutuhkan objek untuk ada).

Sebagai contoh dalam kasus kami, dalam kasus a1dan a2aturan khusus mengatakan bahwa dalam deklarasi tersebut, objek hasil penginisialisasi prvalue dari jenis yang sama seperti a1variabel a1, dan oleh karena itu A_factory_func()secara langsung menginisialisasi objek a1. Setiap pemain gaya fungsional perantara tidak akan memiliki efek apa pun, karena A_factory_func(another-prvalue)hanya "melewati" objek hasil dari nilai luar menjadi juga objek hasil dari nilai dalam.


A a1 = A_factory_func();
A a2(A_factory_func());

Tergantung pada jenis apa yang A_factory_func()dikembalikan. Saya menganggap itu mengembalikan A- kemudian melakukan hal yang sama - kecuali bahwa ketika copy constructor eksplisit, maka yang pertama akan gagal. Baca 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Ini melakukan hal yang sama karena ini adalah tipe bawaan (ini berarti bukan tipe kelas di sini). Baca 8.6 / 14 .

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

Ini tidak melakukan hal yang sama. Default-inisialisasi pertama Aadalah non-POD, dan tidak melakukan inisialisasi untuk POD (Baca 8.6 / 9 ). Salinan kedua dimulai: Nilai-menginisialisasi sementara dan kemudian menyalin nilai itu ke c2(Baca 5.2.3 / 2 dan 8.6 / 14 ). Ini tentu saja membutuhkan konstruktor salinan non-eksplisit (Baca 8.6 / 14 dan 12.3.1 / 3 dan 13.3.1.3/1 ). Yang ketiga membuat deklarasi fungsi untuk fungsi c3yang mengembalikan Adan yang membawa penunjuk fungsi ke fungsi yang mengembalikan a A(Baca 8.2 ).


Menggali Inisialisasi Langsung dan Salin inisialisasi

Walaupun mereka terlihat identik dan seharusnya melakukan hal yang sama, kedua bentuk ini sangat berbeda dalam kasus-kasus tertentu. Dua bentuk inisialisasi adalah langsung dan salin inisialisasi:

T t(x);
T t = x;

Ada perilaku yang dapat kita atributkan untuk masing-masing:

  • Inisialisasi langsung berperilaku seperti pemanggilan fungsi ke fungsi kelebihan beban: Fungsi, dalam hal ini, adalah konstruktor T(termasuk explicityang), dan argumennya adalah x. Resolusi kelebihan akan menemukan konstruktor yang paling cocok, dan ketika dibutuhkan akan melakukan konversi implisit yang diperlukan.
  • Salin inisialisasi membuat urutan konversi tersirat: Mencoba mengonversi xke objek tipe T. (Ini kemudian dapat menyalin objek tersebut ke objek yang diinisialisasi, sehingga konstruktor salinan juga diperlukan - tetapi ini tidak penting di bawah)

Seperti yang Anda lihat, salin inisialisasi dalam beberapa cara merupakan bagian dari inisialisasi langsung sehubungan dengan kemungkinan konversi tersirat: Sementara inisialisasi langsung memiliki semua konstruktor yang tersedia untuk dipanggil, dan selain itu dapat melakukan konversi tersirat yang diperlukan untuk mencocokkan jenis argumen, salin inisialisasi hanya dapat mengatur satu urutan konversi implisit.

Saya berusaha keras dan mendapatkan kode berikut untuk menampilkan teks yang berbeda untuk masing-masing bentuk , tanpa menggunakan "jelas" melalui explicitkonstruktor.

#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>

Bagaimana cara kerjanya, dan mengapa itu menghasilkan hasil itu?

  1. Inisialisasi langsung

    Pertama tidak tahu apa-apa tentang konversi. Itu hanya akan mencoba memanggil konstruktor. Dalam hal ini, konstruktor berikut tersedia dan merupakan pasangan yang tepat :

    B(A const&)

    Tidak ada konversi, apalagi konversi yang ditentukan pengguna, diperlukan untuk memanggil konstruktor itu (perhatikan bahwa tidak ada konversi kualifikasi konstanta yang terjadi di sini juga). Dan inisialisasi langsung akan menyebutnya.

  2. Salin inisialisasi

    Seperti yang dikatakan di atas, salin inisialisasi akan membuat urutan konversi ketika abelum mengetik Batau berasal darinya (yang jelas terjadi di sini). Jadi akan mencari cara untuk melakukan konversi, dan akan menemukan kandidat berikut

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

    Perhatikan bagaimana saya menulis ulang fungsi konversi: Jenis parameter mencerminkan jenis thispenunjuk, yang dalam fungsi non-const adalah untuk non-const. Sekarang, kami menyebut para kandidat ini dengan xargumen. Pemenangnya adalah fungsi konversi: Karena jika kita memiliki dua fungsi kandidat yang sama-sama menerima referensi ke tipe yang sama, maka versi const yang lebih sedikit menang (ini, omong-omong, juga mekanisme yang lebih memilih fungsi anggota non-const meminta objek -const).

    Perhatikan bahwa jika kita mengubah fungsi konversi menjadi fungsi const member, maka konversi tersebut ambigu (karena keduanya memiliki tipe parameter A const&saat itu): Kompilator Comeau menolaknya dengan benar, tetapi GCC menerimanya dalam mode non-pedantic. Beralih ke -pedanticmembuatnya menghasilkan peringatan ambiguitas yang tepat juga.

Saya harap ini agak membantu untuk memperjelas perbedaan kedua bentuk ini!


Wow. Saya bahkan tidak menyadari tentang deklarasi fungsi. Saya cukup banyak harus menerima jawaban Anda hanya untuk menjadi satu-satunya yang tahu tentang itu. Apakah ada alasan bahwa deklarasi fungsi berfungsi seperti itu? Akan lebih baik jika c3 diperlakukan berbeda di dalam suatu fungsi.
rlbond

4
Bah, maaf teman-teman, tetapi saya harus menghapus komentar saya dan mempostingnya lagi, karena mesin pemformatan baru: Itu karena dalam parameter fungsi, R() == R(*)()dan T[] == T*. Yaitu, tipe fungsi adalah tipe pointer fungsi, dan tipe array adalah tipe pointer-to-elemen. Ini menyebalkan. Ini dapat diatasi dengan A c3((A()));(parens sekitar ekspresi).
Johannes Schaub - litb

4
Bolehkah saya bertanya apa artinya "'Baca 8.5 / 14'"? Apa yang dimaksud dengan itu? Buku? Bab? Sebuah situs?
AzP

9
@AzP banyak orang di SO sering ingin referensi ke spesifikasi C ++, dan itulah yang saya lakukan di sini, sebagai tanggapan atas permintaan rlbond "Tolong kutip teks sebagai bukti.". Saya tidak ingin mengutip spec, karena itu membengkak jawaban saya dan lebih banyak pekerjaan untuk tetap up to date (redundansi).
Johannes Schaub - litb

1
@ luca saya sarankan untuk memulai pertanyaan baru untuk itu sehingga orang lain dapat memperoleh manfaat dari jawaban yang diberikan orang
Johannes Schaub - litb

49

Tugas berbeda dari inisialisasi .

Kedua baris berikut melakukan inisialisasi . Panggilan konstruktor tunggal dilakukan:

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

tapi itu tidak setara dengan:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

Saya tidak memiliki teks saat ini untuk membuktikan ini, tetapi sangat mudah untuk bereksperimen:

#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
Referensi yang baik: "Bahasa Pemrograman C ++, Edisi Khusus" oleh Bjarne Stroustrup, bagian 10.4.4.1 (halaman 245). Menjelaskan inisialisasi salin dan tugas penyalinan dan mengapa mereka berbeda secara mendasar (meskipun keduanya menggunakan operator = sebagai sintaks).
Naaff

Minor nit, tetapi saya benar-benar tidak suka ketika orang mengatakan bahwa "A (x)" dan "A = x" sama. Sebenarnya tidak. Dalam banyak kasus mereka akan melakukan hal yang persis sama tetapi dimungkinkan untuk membuat contoh di mana tergantung pada argumen konstruktor yang berbeda sebenarnya disebut.
Richard Corden

Saya tidak berbicara tentang "kesetaraan sintaksis." Secara semantik, kedua cara inisialisasi adalah sama.
Mehrdad Afshari

@MehrdadAfshari Dalam kode jawaban Johannes Anda mendapatkan keluaran berbeda berdasarkan yang mana dari dua yang Anda gunakan.
Brian Gordon

1
@BrianGordon Ya, Anda benar. Mereka tidak setara. Saya telah membahas komentar Richard di edit saya sejak lama.
Mehrdad Afshari

22

double b1 = 0.5; adalah panggilan implisit dari konstruktor.

double b2(0.5); adalah panggilan eksplisit.

Lihatlah kode berikut untuk melihat perbedaannya:

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

Jika kelas Anda tidak memiliki konstruktor eksplisit daripada panggilan eksplisit dan implisit adalah identik.


5
+1. Jawaban bagus. Bagus juga mencatat versi eksplisit. Ngomong-ngomong, penting untuk dicatat bahwa Anda tidak dapat memiliki kedua versi kelebihan konstruktor tunggal secara bersamaan. Jadi, itu hanya akan gagal untuk dikompilasi dalam kasus eksplisit. Jika keduanya dikompilasi, mereka harus berperilaku serupa.
Mehrdad Afshari

4

Pengelompokan pertama: itu tergantung pada apa yang A_factory_funckembali. Baris pertama adalah contoh inisialisasi salin , baris kedua adalah inisialisasi langsung . Jika A_factory_funcmengembalikan Aobjek maka mereka setara, mereka berdua memanggil konstruktor salin untuk A, jika tidak versi pertama membuat nilai tipe Adari operator konversi yang tersedia untuk tipe kembali A_factory_funcatau Akonstruktor yang sesuai , dan kemudian memanggil konstruktor salin untuk membangun a1dari ini sementara. Versi kedua mencoba untuk menemukan konstruktor yang cocok yang mengambil A_factory_funcpengembalian apa pun , atau yang mengambil sesuatu yang nilai pengembaliannya dapat secara implisit dikonversi.

Pengelompokan kedua: logika yang persis sama berlaku, kecuali bahwa tipe yang dibangun tidak memiliki konstruktor yang eksotis sehingga dalam praktiknya identik.

Pengelompokan ketiga: c1default diinisialisasi, c2disalin-diinisialisasi dari nilai yang diinisialisasi sementara. Setiap anggota c1yang memiliki tipe pod (atau anggota anggota, dll., Dll.) Mungkin tidak diinisialisasi jika pengguna memberikan konstruktor default (jika ada) tidak menginisialisasi mereka secara eksplisit. Sebab c2, itu tergantung pada apakah ada pengguna yang menyediakan salinan konstruktor dan apakah yang secara tepat menginisialisasi anggota tersebut, tetapi anggota sementara semua akan diinisialisasi (nol diinisialisasi jika tidak diinisialisasi secara eksplisit). Seperti litb yang terlihat, c3adalah jebakan. Ini sebenarnya adalah deklarasi fungsi.


4

Catatan:

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

Yaitu, untuk inisialisasi salin.

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

Dengan kata lain, kompiler yang baik tidak akan membuat salinan untuk inisialisasi salin jika dapat dihindari; sebagai gantinya hanya akan memanggil konstruktor secara langsung - yaitu, seperti untuk inisialisasi langsung.

Dengan kata lain, inisialisasi salin sama seperti inisialisasi langsung dalam kebanyakan kasus <opinion> di mana kode yang dapat dimengerti telah ditulis. Karena inisialisasi langsung berpotensi menyebabkan konversi sewenang-wenang (dan karena itu mungkin tidak diketahui), saya lebih suka untuk selalu menggunakan inisialisasi salin jika memungkinkan. (Dengan bonus itu sebenarnya seperti inisialisasi.) </opinion>

Kegagalan teknis: [12.2 / 1 lanj dari dari atas] 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.

Senang saya tidak menulis kompiler C ++.


4

Anda bisa melihat perbedaannya dalam tipe explicitdan implicitkonstruktor ketika Anda menginisialisasi objek:

Kelas:

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

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

Dan dalam main fungsinya:

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
}

Secara default, konstruktor adalah implicitdemikian Anda memiliki dua cara untuk menginisialisasi:

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization

Dan dengan mendefinisikan struktur explicithanya Anda memiliki satu cara langsung:

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

Menjawab sehubungan dengan bagian ini:

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

Karena sebagian besar jawabannya adalah pra-c ++ 11 saya menambahkan apa yang dikatakan c ++ 11 tentang ini:

Sebuah specifier tipe-sederhana (7.1.6.2) atau specename-specifier (14.6) diikuti oleh daftar ekspresi terkurung menyusun nilai dari tipe yang ditentukan diberikan daftar ekspresi. Jika daftar ekspresi adalah ekspresi tunggal, ekspresi konversi tipe adalah ekuivalen (dalam definisi, dan jika didefinisikan dalam arti) dengan ekspresi cast yang sesuai (5.4). Jika tipe yang ditentukan adalah tipe kelas, tipe kelas harus lengkap. Jika daftar ekspresi menentukan lebih dari satu nilai tunggal, jenisnya harus kelas dengan konstruktor yang dinyatakan sesuai (8.5, 12.1), dan ekspresi T (x1, x2, ...) setara dengan efek pada deklarasi T t (x1, x2, ...); untuk beberapa variabel sementara t diciptakan, dengan hasilnya menjadi nilai t sebagai nilai awal.

Jadi optimasi atau tidak mereka setara sesuai standar. Perhatikan bahwa ini sesuai dengan apa yang disebutkan oleh jawaban lain. Mengutip apa yang dikatakan standar demi kebenaran.


"Daftar ekspresi contoh Anda tidak menentukan lebih dari satu nilai". Bagaimana semua ini relevan?
underscore_d

0

Banyak kasus-kasus ini tunduk pada implementasi objek sehingga sulit untuk memberikan jawaban yang konkret.

Pertimbangkan kopernya

A a = 5;
A a(5);

Dalam hal ini dengan asumsi operator penugasan yang tepat & inisialisasi konstruktor yang menerima argumen integer tunggal, bagaimana saya menerapkan metode tersebut mempengaruhi perilaku setiap baris. Namun itu adalah praktik umum bagi salah satu dari mereka untuk memanggil yang lain dalam implementasi untuk menghilangkan kode duplikat (meskipun dalam kasus sesederhana ini tidak akan ada tujuan nyata.)

Sunting: Seperti disebutkan dalam respons lain, baris pertama sebenarnya akan memanggil pembuat salinan. Pertimbangkan komentar yang berkaitan dengan operator penugasan sebagai perilaku yang berkaitan dengan penugasan yang berdiri sendiri.

Yang mengatakan, bagaimana kompiler mengoptimalkan kode maka akan memiliki dampaknya sendiri. Jika saya memiliki konstruktor inisialisasi yang memanggil operator "=" - jika kompiler tidak membuat optimasi, baris paling atas kemudian akan melakukan 2 lompatan sebagai lawan satu di baris bawah.

Sekarang, untuk situasi yang paling umum, kompiler Anda akan mengoptimalkan melalui kasus-kasus ini dan menghilangkan jenis inefisiensi ini. Jadi secara efektif semua situasi berbeda yang Anda gambarkan akan menjadi sama. Jika Anda ingin melihat dengan tepat apa yang sedang dilakukan, Anda dapat melihat kode objek atau output perakitan dari kompiler Anda.


Ini bukan optimasi . Compiler harus memanggil konstruktor dalam kedua kasus tersebut. Akibatnya, tidak satu pun dari mereka yang dapat dikompilasi jika Anda hanya memiliki operator =(const int)dan tidak A(const int). Lihat jawaban @ jia3ep untuk lebih jelasnya.
Mehrdad Afshari

Saya yakin Anda benar. Namun itu akan dikompilasi dengan baik menggunakan konstruktor copy default.
dborba

Juga, seperti yang saya sebutkan, itu adalah praktik umum untuk memiliki copy constructor memanggil operator penugasan, di mana titik optimasi kompilator ikut bermain.
dborba

0

Ini dari Bahasa Pemrograman C ++ oleh Bjarne Stroustrup:

Inisialisasi dengan = dianggap sebagai inisialisasi salinan . Pada prinsipnya, salinan penginisialisasi (objek tempat kita menyalin) ditempatkan ke objek yang diinisialisasi. Namun, salinan tersebut dapat dioptimalkan jauh (elided), dan operasi perpindahan (berdasarkan semantik langkah) dapat digunakan jika penginisialisasi adalah nilai. Meninggalkan = membuat inisialisasi eksplisit. Inisialisasi eksplisit dikenal sebagai inisialisasi langsung .

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.