Mari kita bayangkan kita memiliki sebuah struct untuk menampung 3 ganda dengan beberapa fungsi anggota:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
Ini sedikit dibuat-buat untuk kesederhanaan, tetapi saya yakin Anda setuju bahwa kode serupa ada di luar sana. Metode ini memungkinkan Anda untuk merangkai dengan mudah, misalnya:
Vector v = ...;
v.normalize().negate();
Atau bahkan:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Sekarang jika kita menyediakan fungsi begin () dan end (), kita bisa menggunakan Vector kita dalam gaya baru for loop, katakanlah untuk melakukan loop pada 3 koordinat x, y, dan z (Anda pasti bisa membuat contoh yang lebih "berguna" dengan mengganti Vector dengan misalnya String):
Vector v = ...;
for (double x : v) { ... }
Kami bahkan dapat melakukan:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
dan juga:
for (double x : Vector{1., 2., 3.}) { ... }
Namun, berikut ini (menurut saya) rusak:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Meskipun sepertinya kombinasi logis dari dua penggunaan sebelumnya, saya pikir penggunaan terakhir ini menciptakan referensi yang menggantung sementara dua penggunaan sebelumnya baik-baik saja.
- Apakah ini benar dan dihargai secara luas?
- Bagian mana di atas yang merupakan bagian "buruk", yang harus dihindari?
- Akankah bahasa ditingkatkan dengan mengubah definisi loop berbasis rentang sedemikian rupa sehingga sementara yang dibangun dalam ekspresi-for ada selama durasi loop?
Jawaban:
Ya, pemahaman Anda tentang berbagai hal sudah benar.
Bagian yang buruk adalah mengambil referensi nilai-l ke sementara yang dikembalikan dari suatu fungsi, dan mengikatnya ke referensi nilai-r. Ini sama buruknya dengan ini:
auto &&t = Vector{1., 2., 3.}.normalize();
Masa
Vector{1., 2., 3.}
pakai sementara tidak dapat diperpanjang karena kompilator tidak tahu bahwa nilai yang dikembalikan darinormalize
referensi itu.Itu akan sangat tidak konsisten dengan cara kerja C ++.
Akankah itu mencegah gotcha tertentu yang dibuat oleh orang-orang yang menggunakan ekspresi dirantai pada sementara atau berbagai metode evaluasi malas untuk ekspresi? Iya. Tetapi itu juga akan membutuhkan kode kompilator kasus khusus, serta membingungkan mengapa itu tidak bekerja dengan yang lain konstruksi ekspresi .
Solusi yang jauh lebih masuk akal adalah dengan memberi tahu kompiler bahwa nilai kembalian dari suatu fungsi selalu menjadi rujukan
this
, dan oleh karena itu jika nilai kembalian terikat ke konstruksi perluasan sementara, maka itu akan memperpanjang nilai sementara yang benar. Itu adalah solusi tingkat bahasa.Saat ini (jika kompilator mendukungnya), Anda bisa membuatnya sehingga
normalize
tidak bisa dipanggil sementara:struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector &normalize() && = delete; };
Ini akan menyebabkan
Vector{1., 2., 3.}.normalize()
memberikan kesalahan kompilasi, sementarav.normalize()
akan berfungsi dengan baik. Jelas Anda tidak akan bisa melakukan hal-hal yang benar seperti ini:Vector t = Vector{1., 2., 3.}.normalize();
Tapi Anda juga tidak akan bisa melakukan hal yang salah.
Alternatifnya, seperti yang disarankan dalam komentar, Anda dapat membuat versi referensi rvalue mengembalikan nilai daripada referensi:
struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector normalize() && { Vector ret = *this; ret.normalize(); return ret; } };
Jika
Vector
adalah tipe dengan sumber daya yang sebenarnya untuk dipindahkan, Anda dapat menggunakanVector ret = std::move(*this);
sebagai gantinya. Optimalisasi nilai pengembalian bernama membuat ini cukup optimal dalam hal kinerja.sumber
delete
Anda dapat memberikan operasi alternatif yang mengembalikan nilai r:Vector normalize() && { normalize(); return std::move(*this); }
(Saya percaya bahwa panggilan kenormalize
dalam fungsi akan dikirim ke lvalue overload, tetapi seseorang harus memeriksanya :)&
/&&
kualifikasi ini. Apakah ini dari C ++ 11 atau ini beberapa ekstensi kompiler berpemilik (mungkin tersebar luas). Memberikan kemungkinan yang menarik.Itu bukan batasan bahasa, tapi masalah dengan kode Anda. Ekspresi
Vector{1., 2., 3.}
membuat sementara, tetapinormalize
fungsi mengembalikan referensi lvalue . Karena ekspresinya adalah nilai l , kompilator mengasumsikan bahwa objek akan hidup, tetapi karena ini adalah referensi untuk sementara, objek mati setelah ekspresi penuh dievaluasi, jadi Anda akan mendapatkan referensi yang menggantung.Sekarang, jika Anda mengubah desain Anda untuk mengembalikan objek baru berdasarkan nilai daripada referensi ke objek saat ini, maka tidak akan ada masalah dan kode akan bekerja seperti yang diharapkan.
sumber
const
referensi akan memperpanjang umur objek dalam kasus ini?normalize()
sebagai fungsi mutasi pada objek yang ada. Jadi pertanyaannya. Bahwa sementara memiliki "masa hidup yang diperpanjang" ketika digunakan untuk tujuan tertentu dari sebuah iterasi, dan bukan sebaliknya, menurut saya adalah kesalahan fitur yang membingungkan.const&
) diperpanjang masa pakainya.Vector & r = Vector{1.,2.,3.}.normalize();
. Desain Anda memiliki batasan itu, dan itu berarti Anda bersedia mengembalikan dengan nilai (yang mungkin masuk akal dalam banyak keadaan, dan terlebih lagi dengan rvalue-referensi dan pemindahan ), atau Anda perlu menangani masalah di tempat call: buat variabel yang tepat, lalu gunakan di for loop. Perhatikan juga bahwa ekspresiVector v = Vector{1., 2., 3.}.normalize().negate();
membuat dua objek ...T const& f(T const&);
sepenuhnya baik-baik saja.T const& t = f(T());
baik-baik saja. Dan kemudian, di TU lain Anda menemukan ituT const& f(T const& t) { return t; }
dan Anda menangis ... Jikaoperator+
beroperasi pada nilai-nilai, itu lebih aman ; maka kompilator dapat mengoptimalkan salinan (Want Speed? Pass by Values), tapi itu bonus. Satu-satunya pengikatan sementara yang saya izinkan adalah pengikatan ke referensi nilai-r, tetapi fungsi kemudian harus mengembalikan nilai untuk keamanan dan mengandalkan Salin Elisi / Semantik Pindah.IMHO, contoh kedua sudah cacat. Operator pengubah mengembalikan
*this
nyaman dengan cara yang Anda sebutkan: memungkinkan rantai pengubah. Ini dapat digunakan hanya dengan menyerahkan hasil modifikasi, tetapi melakukan ini rawan kesalahan karena dapat dengan mudah diabaikan. Jika saya melihat sesuatu sepertiVector v{1., 2., 3.}; auto foo = somefunction1(v, 17); auto bar = somefunction2(true, v, 2, foo); auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));
Saya tidak secara otomatis mencurigai bahwa fungsi berubah
v
sebagai efek samping. Tentu saja bisa , tapi akan membingungkan. Jadi jika saya menulis sesuatu seperti ini, saya akan memastikan ituv
tetap konstan. Sebagai contoh Anda, saya akan menambahkan fungsi gratisauto normalized(Vector v) -> Vector {return v.normalize();} auto negated(Vector v) -> Vector {return v.negate();}
dan kemudian tulis loop
for( double x : negated(normalized(v)) ) { ... }
dan
for( double x : normalized(Vector{1., 2., 3}) ) { ... }
Itu IMO lebih mudah dibaca, dan lebih aman. Tentu saja, ini memerlukan salinan tambahan, namun untuk data yang dialokasikan dengan heap, hal ini mungkin dapat dilakukan dalam operasi pemindahan C ++ 11 yang murah.
sumber