Kategori nilai, dan referensi ke kategori tersebut

Topik ini memperkenalkan dan menjelaskan berbagai kategori nilai (dan referensi ke nilai) yang ada di C++:

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Anda tidak akan diragukan lagi telah mendengar lvalues dan rvalues. Tetapi Anda mungkin tidak memikirkannya dalam istilah yang disajikan topik ini.

Setiap ekspresi dalam C++ menghasilkan nilai milik salah satu dari lima kategori yang tercantum di atas. Ada aspek bahasa C++ —fasilitas dan aturannya—yang menuntut pemahaman yang tepat tentang kategori nilai ini, serta referensinya. Aspek-aspek ini termasuk mengambil alamat nilai, menyalin nilai, memindahkan nilai, dan meneruskan nilai ke fungsi lain. Topik ini tidak masuk ke semua aspek tersebut secara mendalam, tetapi memberikan informasi dasar untuk pemahaman yang kuat tentang mereka.

Info dalam topik ini dibingkai dalam hal analisis kategori nilai Stroustrup oleh dua properti independen identitas dan movabilitas [Stroustrup, 2013].

Lvalue memiliki identitas

Apa artinya nilai memiliki identitas? Jika Anda memiliki (atau Anda dapat mengambil) alamat memori nilai, dan menggunakannya dengan aman, maka nilai memiliki identitas. Dengan begitu, Anda dapat melakukan lebih dari membandingkan konten nilai—Anda dapat membandingkan atau membedakannya berdasarkan identitas.

Sebuah lvalue memiliki identitas. Sekarang hanya masalah minat historis bahwa "l" dalam "lvalue" adalah singkatan dari "kiri" (seperti di, sisi kiri tugas). Di C++, lvalue dapat muncul di sebelah kiri atau di sebelah kanan penugasan. "L" dalam "lvalue", kemudian, tidak benar-benar membantu Anda untuk memahami atau mendefinisikan apa mereka. Anda hanya perlu memahami bahwa apa yang kita sebut lvalue adalah nilai yang memiliki identitas.

Contoh ekspresi yang merupakan lvalues meliputi: variabel atau konstanta bernama; atau fungsi yang mengembalikan referensi. Contoh ekspresi yang tidak lvalues meliputi: sementara; atau fungsi yang dikembalikan berdasarkan nilai.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

Sekarang, meskipun itu adalah pernyataan yang benar bahwa lvalues memiliki identitas, itu juga benar dari xvalues. Kita akan membahas dengan tepat apa yang xvalue nanti dalam topik ini. Untuk saat ini, perlu diketahui bahwa ada kategori nilai yang disebut glvalue (untuk "generalized lvalue"). Set lem adalah superset dari kedua lvalue (juga dikenal sebagai lvalue klasik) dan xvalues. Jadi, sementara "lvalue memiliki identitas" adalah benar, kumpulan lengkap hal-hal yang memiliki identitas adalah set glvalue, seperti yang ditunjukkan dalam ilustrasi ini.

An lvalue has identity

Rvalue dapat bergerak; sebuah lvalue tidak

Tetapi ada nilai yang bukan glvalue. Dengan kata lain, ada nilai yang tidak dapat Anda dapatkan alamat memorinya (atau Anda tidak dapat mengandalkannya agar valid). Kami melihat beberapa nilai tersebut dalam contoh kode di atas.

Tidak memiliki alamat memori yang dapat diandalkan terdengar seperti kerugian. Tetapi sebenarnya keuntungan dari nilai seperti itu adalah Anda dapat memindahkannya (yang umumnya murah), daripada menyalinnya (yang umumnya mahal). Memindahkan nilai berarti nilai tersebut tidak lagi berada di tempat yang seharusnya. Jadi mencoba mengaksesnya di tempat di mana dulunya adalah sesuatu yang harus dihindari. Diskusi tentang kapan dan cara memindahkan nilai berada di luar cakupan untuk topik ini. Untuk topik ini, kita hanya perlu tahu bahwa nilai movable dikenal sebagai rvalue (atau rvalue klasik).

"r" dalam "rvalue" adalah singkatan dari "kanan" (seperti di, sisi kanan tugas). Tetapi Anda dapat menggunakan rvalue, dan referensi ke rvalue, di luar penugasan. "r" dalam "rvalue", maka, bukan hal yang harus difokuskan. Anda hanya perlu memahami bahwa apa yang kita sebut rvalue adalah nilai yang dapat bergerak.

Sebuah lvalue, sebaliknya, tidak bergerak, seperti yang ditunjukkan dalam ilustrasi ini. Jika lvalue untuk bergerak, maka itu akan bertentangan dengan definisi yang sangat lvalue. Dan itu akan menjadi masalah tak terduga untuk kode yang sangat wajar diharapkan untuk dapat terus mengakses lvalue.

An rvalue is movable; an lvalue is not

Jadi anda tidak bisa memindahkan sebuah lvalue. Tetapi ada semacam glvalue (kumpulan hal-hal dengan identitas) yang dapat Anda pindahkan—jika Anda tahu apa yang Anda lakukan (termasuk berhati-hati untuk tidak mengaksesnya setelah pemindahan)—dan itu adalah xvalue. Kita akan mengunjungi kembali ide itu sekali lagi nanti dalam topik ini ketika kita melihat gambaran lengkap kategori nilai.

Referensi rvalue, dan aturan pengikatan referensi

Bagian ini memperkenalkan sintaks untuk referensi ke rvalue. Kita harus menunggu topik lain masuk ke perlakuan substansial untuk bergerak dan meneruskan, tetapi cukup untuk mengatakan bahwa referensi rvalue adalah bagian yang diperlukan dari solusi masalah tersebut. Namun, sebelum kita melihat referensi rvalue, pertama-tama kita harus lebih T&jelas —hal yang sebelumnya kita sebut hanya "referensi". Ini benar-benar "referensi lvalue (non-const)," yang mengacu pada nilai yang dapat ditulis pengguna referensi.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Referensi lvalue dapat mengikat ke lvalue, tetapi tidak ke rvalue.

Kemudian ada referensi const lvalue (T const&), yang merujuk ke objek yang tidak dapat ditulis pengguna referensi (misalnya, konstanta).

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Referensi const lvalue dapat mengikat ke lvalue atau ke rvalue.

Sintaks untuk referensi ke rvalue jenis T ditulis sebagai T&&. Referensi rvalue mengacu pada nilai movable—nilai yang kontennya tidak perlu kami pertahankan setelah kami menggunakannya (misalnya, sementara). Karena seluruh titik adalah berpindah dari (sehingga memodifikasi) nilai yang terikat ke referensi rvalue, const dan volatile kualifikasi (juga dikenal sebagai cv-qualifier) tidak berlaku untuk referensi rvalue.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Referensi rvalue mengikat ke rvalue. Bahkan, dalam hal resolusi kelebihan beban, rvalue lebih suka terikat ke referensi rvalue daripada ke referensi const lvalue. Tetapi referensi rvalue tidak dapat mengikat ke lvalue karena, seperti yang telah kami katakan, referensi rvalue mengacu pada nilai yang kontennya diasumsikan kami tidak perlu mempertahankan (misalnya, parameter untuk konstruktor pemindahan).

Anda juga dapat melewati rvalue di mana argumen menurut nilai diharapkan, melalui konstruksi salinan (atau melalui konstruksi pemindahan jika rvalue adalah xvalue).

Glvalue memiliki identitas; prvalue tidak

Pada tahap ini, kita tahu apa yang memiliki identitas. Dan kita tahu apa yang bergerak dan apa yang tidak. Tetapi kami belum menamai sekumpulan nilai yang belum memiliki identitas. Set itu dikenal sebagai prvalue, atau rvalue murni.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

A glvalue has identity; a prvalue does not

Gambar lengkap kategori nilai

Hanya tetap menggabungkan info dan ilustrasi di atas menjadi satu gambaran besar.

The complete picture of value categories

glvalue (i)

Glvalue (generalized lvalue) memiliki identitas. Kita akan menggunakan "i" sebagai singkatan untuk "memiliki identitas".

lvalue (i&!m)

Lvalue (semacam glvalue) memiliki identitas, tetapi tidak dapat bergerak. Ini biasanya merupakan nilai baca-tulis yang Anda berikan melalui referensi atau referensi const, atau berdasarkan nilai jika penyalinan murah. Lvalue tidak dapat terikat ke referensi rvalue.

xvalue (i&m)

Xvalue (semacam glvalue, tetapi juga semacam rvalue) memiliki identitas, dan juga dapat bergerak. Ini mungkin ivalue sementara bahwa Anda telah memutuskan untuk pindah karena penyalinan mahal, dan Anda akan berhati-hati untuk tidak mengaksesnya setelahnya. Berikut adalah cara Anda dapat mengubah lvalue menjadi xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

Dalam contoh kode di atas, kami belum memindahkan apa pun. Kami hanya membuat xvalue dengan mentransmisikan lvalue ke referensi rvalue yang tidak disebutkan namanya. Ini masih dapat diidentifikasi dengan nama lvalue-nya; tetapi, sebagai xvalue, sekarang mampu dipindahkan. Alasan untuk memindahkannya, dan seperti apa perpindahannya, harus menunggu topik lain. Tetapi Anda dapat menganggap "x" dalam "xvalue" sebagai arti "khusus ahli" jika itu membantu. Dengan mentransmisikan lvalue ke dalam xvalue (semacam rvalue, ingat), nilai kemudian menjadi mampu terikat ke referensi rvalue.

Berikut adalah dua contoh xvalue lainnya—memanggil fungsi yang mengembalikan referensi rvalue yang tidak disebutkan namanya, dan mengakses anggota xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Prvalue (rvalue murni; semacam rvalue) tidak memiliki identitas, tetapi dapat bergerak. Ini biasanya bersifat sementara, atau hasil dari memanggil fungsi yang dikembalikan berdasarkan nilai, atau hasil dari mengevaluasi ekspresi lain yang bukan glvalue.

rvalue (m)

Sebuah rvalue dapat bergerak. Kita akan menggunakan "m" sebagai singkatan untuk "dapat bergerak".

Referensi rvalue selalu mengacu pada rvalue (nilai yang kontennya diasumsikan tidak perlu kami pertahankan).

Tapi, apakah referensi rvalue itu sendiri adalah rvalue? Referensi rvalue yang tidak disebutkan namanya (seperti yang ditunjukkan dalam contoh kode xvalue di atas) adalah xvalue jadi, ya, itu adalah rvalue. Ini lebih suka terikat ke parameter fungsi referensi rvalue, seperti konstruktor pemindahan. Sebaliknya (dan mungkin berlawanan-intuitif), jika referensi rvalue memiliki nama, maka ekspresi yang terdiri dari nama tersebut adalah lvalue. Jadi tidak dapat terikat ke parameter referensi rvalue. Tetapi mudah untuk membuatnya melakukannya—cukup transmisikan ke referensi rvalue yang tidak disebutkan namanya (xvalue) lagi.

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Jenis nilai yang tidak memiliki identitas dan tidak dapat bergerak adalah salah satu kombinasi yang belum kita bahas. Tetapi kita dapat mengalihkannya, karena kategori itu bukan ide yang berguna dalam bahasa C++.

Aturan penciutkan referensi

Beberapa referensi seperti dalam ekspresi (referensi lvalue ke referensi lvalue, atau referensi rvalue ke referensi rvalue) membatalkan satu sama lain.

  • A& & menciutkan ke dalam A&.
  • A&& && menciutkan ke dalam A&&.

Beberapa referensi tidak seperti dalam ekspresi yang diciutkan ke referensi lvalue.

  • A& && menciutkan ke dalam A&.
  • A&& & menciutkan ke dalam A&.

Referensi penerusan

Bagian akhir ini membedakan referensi rvalue, yang telah kita bahas, dengan konsep referensi penerusan yang berbeda. Sebelum istilah "referensi penerusan" dikoinkan, beberapa orang menggunakan istilah "referensi universal".

void foo(A&& a) { ... }
  • A&& adalah referensi rvalue, seperti yang telah kita lihat. Const dan volatile tidak berlaku untuk referensi rvalue.
  • foo hanya menerima rvalue tipe A.
  • Alasan referensi rvalue (seperti A&&) ada sehingga Anda dapat menulis kelebihan beban yang dioptimalkan untuk kasus sementara (atau rvalue lainnya) yang diteruskan.
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& adalah referensi penerusan. Bergantung pada apa yang Anda teruskan ke bar, ketik _Ty bisa menjadi const/non-const secara independen dari volatil/non-volatile.
  • bar menerima lvalue atau rvalue jenis _Ty.
  • Meneruskan lvalue menyebabkan referensi penerusan menjadi _Ty& &&, yang diciutkan ke referensi _Ty&lvalue .
  • Meneruskan rvalue menyebabkan referensi penerusan menjadi referensi _Ty&&rvalue .
  • Referensi penerusan alasan (seperti _Ty&&) ada bukan untuk pengoptimalan, tetapi untuk mengambil apa yang Anda berikan kepada mereka dan untuk meneruskannya secara transparan dan efisien. Anda mungkin menemukan referensi penerusan hanya jika Anda menulis (atau mempelajari dengan cermat) kode pustaka—misalnya, fungsi pabrik yang diteruskan pada argumen konstruktor.

Sumber

  • [Stroustrup, 2013] B. Stroustrup: Bahasa Pemrograman C++, Edisi Keempat. Addison-Wesley. 2013.