Bagaimana cara kerja penghapusan salinan yang dijamin?

90

Pada pertemuan Oulu ISO C ++ Standards 2016, sebuah proposal yang disebut Menjamin penghapusan salinan melalui kategori nilai yang disederhanakan dipilih ke dalam C ++ 17 oleh komite standar.

Bagaimana tepatnya cara kerja penghapusan salinan yang dijamin? Apakah itu mencakup beberapa kasus di mana penghapusan salinan sudah diizinkan, atau apakah perubahan kode diperlukan untuk menjamin penghapusan salinan?

jotik
sumber

Jawaban:

130

Penghapusan salinan diizinkan terjadi dalam sejumlah keadaan. Namun, bahkan jika diizinkan, kode tersebut masih harus dapat berfungsi seolah-olah salinannya tidak dihilangkan. Yaitu, harus ada salinan yang dapat diakses dan / atau konstruktor pindahkan.

Penghapusan salinan yang dijamin mengubah sejumlah konsep C ++, sehingga keadaan tertentu di mana salinan / pemindahan dapat dihilangkan tidak benar-benar memicu penyalinan / pemindahan sama sekali . Kompilator tidak meminta salinan; standar mengatakan bahwa penyalinan seperti itu tidak akan pernah terjadi.

Pertimbangkan fungsi ini:

T Func() {return T();}

Di bawah aturan penghapusan salinan tanpa jaminan, ini akan membuat sementara, lalu pindah dari sementara itu ke nilai kembalian fungsi. Operasi pemindahan tersebut dapat dieliminasi, tetapi Tmasih harus memiliki konstruktor pemindahan yang dapat diakses meskipun tidak pernah digunakan.

Demikian pula:

T t = Func();

Ini adalah salinan inisialisasi t. Ini akan menyalin menginisialisasi tdengan nilai kembalian Func. Namun, Ttetap harus memiliki konstruktor bergerak, meskipun tidak akan dipanggil.

Penghapusan salinan yang dijamin mengubah arti dari ekspresi prvalue . Sebelum C ++ 17, prvalues ​​adalah objek sementara. Dalam C ++ 17, ekspresi prvalue hanyalah sesuatu yang dapat terwujud sementara, tetapi belum bersifat sementara.

Jika Anda menggunakan prvalue untuk menginisialisasi objek dari tipe prvalue, maka tidak ada sementara yang terwujud. Saat Anda melakukannya return T();, ini menginisialisasi nilai kembalian fungsi melalui nilai pr. Sejak fungsi itu kembali T, tidak ada sementara yang dibuat; inisialisasi nilai pr hanya secara langsung menilai nilai kembali.

Hal yang perlu dipahami adalah, karena nilai yang dikembalikan adalah nilai pr, maka itu belum menjadi objek . Ini hanyalah sebuah penginisialisasi untuk sebuah objek, seperti T()adanya.

Ketika Anda melakukannya T t = Func();, prvalue dari nilai yang dikembalikan langsung menginisialisasi objek t; tidak ada tahap "buat sementara dan salin / pindahkan". Karena Func()nilai yang dikembalikan adalah prvalue yang setara dengan T(), tlangsung diinisialisasi oleh T(), persis seperti yang Anda lakukan T t = T().

Jika prvalue digunakan dengan cara lain, prvalue akan menjadi objek sementara, yang akan digunakan dalam ekspresi itu (atau dibuang jika tidak ada ekspresi). Jadi jika Anda melakukannya const T &rt = Func();, prvalue akan terwujud sementara (menggunakan T()sebagai penginisialisasi), yang referensinya akan disimpan rt, bersama dengan hal ekstensi seumur hidup sementara yang biasa.

Satu hal yang dijamin elision mengizinkan Anda lakukan adalah mengembalikan objek yang tidak bergerak. Misalnya, lock_guardtidak bisa disalin atau dipindahkan, jadi Anda tidak bisa memiliki fungsi yang mengembalikannya dengan nilai. Tetapi dengan jaminan penghapusan salinan, Anda bisa.

Elision terjamin juga bekerja dengan inisialisasi langsung:

new T(FactoryFunction());

Jika FactoryFunctiondikembalikan Tdengan nilai, ekspresi ini tidak akan menyalin nilai kembali ke memori yang dialokasikan. Ini akan mengalokasikan memori dan menggunakan memori yang dialokasikan sebagai memori nilai kembali untuk pemanggilan fungsi secara langsung.

Jadi fungsi pabrik yang mengembalikan berdasarkan nilai bisa langsung menginisialisasi memori yang dialokasikan heap tanpa menyadarinya. Selama fungsi ini secara internal mengikuti aturan jaminan penghapusan salinan, tentunya. Mereka harus mengembalikan nilai prnilai tipe T.

Tentu saja, ini juga berfungsi:

new auto(FactoryFunction());

Jika Anda tidak suka menulis nama jenis.


Penting untuk diketahui bahwa jaminan di atas hanya berlaku untuk prvalues. Artinya, Anda tidak mendapatkan jaminan saat mengembalikan variabel bernama :

T Func()
{
   T t = ...;
   ...
   return t;
}

Dalam hal ini, tharus masih memiliki konstruktor copy / move yang dapat diakses. Ya, kompilator dapat memilih untuk mengoptimalkan penyalinan / pemindahan. Tetapi compiler masih harus memverifikasi keberadaan konstruktor copy / move yang dapat diakses.

Jadi tidak ada perubahan untuk pengoptimalan nilai pengembalian bernama (NRVO).

Nicol Bolas
sumber
1
@BenVoigt: Menempatkan tipe yang ditentukan pengguna yang tidak dapat disalin secara sepele ke dalam register bukanlah hal yang layak yang dapat dilakukan ABI, apakah elision tersedia atau tidak.
Nicol Bolas
1
Sekarang aturannya publik, mungkin ada baiknya untuk memperbaruinya dengan konsep "prvalues ​​are inisialisasi".
Johannes Schaub - litb
7
@ JohannesSchaub-litb: Ini hanya "ambigu" jika Anda tahu terlalu banyak tentang hal-hal kecil dari standar C ++. Untuk 99% komunitas C ++, kami tahu apa yang dimaksud dengan "penghapusan salinan terjamin". Makalah sebenarnya yang mengusulkan fitur tersebut bahkan berjudul "Jaminan Salinan Elision". Menambahkan "melalui kategori nilai yang disederhanakan" hanya akan membuat pengguna bingung dan sulit untuk memahaminya. Juga keliru, karena aturan ini tidak benar-benar "menyederhanakan" aturan seputar kategori nilai. Suka atau tidak, istilah "penghapusan salinan terjamin" mengacu pada fitur ini dan tidak ada yang lain.
Nicol Bolas
1
Saya sangat ingin dapat mengambil nilai dan membawanya kemana-mana. Saya rasa ini hanya (sekali tembak) std::function<T()>.
Yakk - Adam Nevraumont
1
@LukasSalich: Itu pertanyaan C ++ 11. Jawaban ini tentang fitur C ++ 17.
Nicol Bolas