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.

Informasi dalam topik ini disusun berdasarkan analisis Stroustrup tentang kategori nilai menurut dua properti independen: identitas dan kemampuan untuk dipindahkan [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. Kini, fakta bahwa "l" dalam "lvalue" merupakan singkatan dari "kiri" hanya menarik dari sudut pandang sejarah (yakni, ruas kiri dari suatu penugasan). Di C++, lvalue dapat muncul di sebelah kiri atau di sebelah kanan penugasan. Huruf "l" dalam "lvalue", jadi, sebenarnya tidak membantu Anda memahami ataupun mendefinisikan apa itu. 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 bukan lvalue meliputi: objek sementara; atau fungsi yang mengembalikan 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 apa sebenarnya xvalue itu nanti di topik ini. Untuk saat ini, perlu diketahui bahwa ada kategori nilai yang disebut glvalue (untuk "generalized lvalue"). Himpunan glvalue merupakan superhimpunan yang mencakup lvalue (juga dikenal sebagai lvalue klasik) dan xvalue. Jadi, sementara "lvalue memiliki identitas" adalah benar, kumpulan lengkap hal-hal yang memiliki identitas adalah set glvalue, seperti yang ditunjukkan dalam ilustrasi ini.

Lvalue memiliki identitas

rvalue dapat dipindahkan; lvalue tidak dapat dipindahkan

Namun, 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 sebelumnya berada adalah hal 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 yang dapat dipindahkan disebut rvalue (atau rvalue klasik).

"r" dalam "rvalue" adalah singkatan dari "right" ("kanan"), yaitu sisi kanan dari suatu penugasan. Tetapi Anda dapat menggunakan rvalue, dan referensi ke rvalue, di luar penugasan. Huruf "r" dalam "rvalue", kalau begitu, bukanlah hal yang perlu 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 sebuah lvalue berpindah, hal itu akan bertentangan dengan definisi lvalue itu sendiri. Dan itu akan menjadi masalah yang tidak terduga bagi kode yang sewajarnya mengharapkan untuk tetap dapat mengakses lvalue tersebut.

Rvalue bisa dipindahkan; lvalue tidak

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 untuk membahas secara mendalam tentang move dan forward, namun cukuplah dikatakan bahwa referensi rvalue merupakan bagian penting dari solusi untuk masalah-masalah tersebut. Namun, sebelum kita membahas referensi rvalue, pertama-tama kita perlu lebih jelas tentang T&—hal yang sebelumnya kita sebut sekadar "referensi". Sebenarnya, ini adalah "referensi lvalue (non-const)", yang merujuk ke sebuah nilai yang dapat ditulisi oleh pengguna referensi tersebut.

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 diikat 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 lvalue const dapat diikat ke lvalue maupun rvalue.

Sintaks untuk referensi ke rvalue jenis T ditulis sebagai T&&. Referensi rvalue mengacu pada nilai yang dapat dipindahkan—nilai yang isinya tidak perlu kita pertahankan setelah digunakan (misalnya, objek sementara). Karena inti utamanya adalah memindahkan dari nilai yang terikat pada referensi rvalue (sehingga memodifikasinya), kualifikasi const dan volatile (juga dikenal sebagai kualifikasi cv) 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 terikat pada 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 meneruskan rvalue ketika diharapkan argumen by-value, melalui konstruksi salinan (atau melalui konstruksi pemindahan jika rvalue tersebut merupakan 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.
}

Glvalue memiliki identitas; prvalue tidak

Gambar lengkap kategori nilai

Kini tinggal menggabungkan informasi dan ilustrasi di atas ke dalam satu gambaran besar.

Gambar lengkap kategori nilai

glvalue (i)

Glvalue (generalized lvalue) memiliki identitas. Kita akan menggunakan "i" sebagai kependekan dari "memiliki identitas".

lvalue (i&!m)

Lvalue (semacam glvalue) memiliki identitas, tetapi tidak dapat bergerak. Ini biasanya merupakan nilai baca-tulis yang Anda teruskan sebagai referensi atau referensi konstan, atau sebagai nilai jika biaya penyalinannya rendah. 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 lvalue yang sebelumnya Anda putuskan untuk dipindahkan karena menyalinnya mahal, dan Anda akan berhati-hati untuk tidak mengaksesnya setelah itu. 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; tetapi, sebagai xvalue, kini dapat dipindahkan. Alasan untuk memindahkannya, dan seperti apa sebenarnya proses pemindahannya, akan dibahas di topik lain. Tetapi Anda dapat menganggap "x" dalam "xvalue" sebagai arti "khusus ahli" jika itu membantu. Dengan mengubah lvalue menjadi xvalue (ingat, ini adalah salah satu jenis rvalue), nilai tersebut kemudian dapat diikat 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; sejenis rvalue) tidak memiliki identitas, tetapi dapat dipindahkan. 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 merujuk ke rvalue (nilai yang isinya diasumsikan tidak perlu dipertahankan).

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. Hal ini lebih cenderung diikat ke parameter fungsi berupa referensi rvalue, seperti pada konstruktor pemindahan. Sebaliknya (dan mungkin agak bertentangan dengan intuisi), jika suatu referensi rvalue memiliki nama, maka ekspresi yang dibentuk 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. Namun kita dapat mengabaikannya, karena kategori itu bukan konsep yang berguna dalam bahasa C++.

Aturan peringkasan referensi

Beberapa referensi yang sejenis dalam sebuah ekspresi (referensi lvalue ke referensi lvalue, atau referensi rvalue ke referensi rvalue) saling meniadakan.

  • A& & ciut menjadi A&.
  • A&& && menciut menjadi A&&.

Beberapa referensi yang berbeda jenis dalam sebuah ekspresi akan terurai menjadi referensi lvalue.

  • A& && menciut menjadi A&.
  • A&& & menciut menjadi A&.

Referensi penerusan

Bagian akhir ini membandingkan referensi rvalue, yang sudah kita bahas, dengan konsep lain, yaitu forwarding reference. Sebelum istilah "forwarding reference" diciptakan, sebagian orang menggunakan istilah "universal reference".

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. Tergantung pada apa yang Anda berikan ke bar, tipe _Ty bisa berupa const atau non-const, terlepas dari apakah itu volatile atau non-volatile.
  • bar menerima lvalue atau rvalue apa pun yang bertipe _Ty.
  • Meneruskan lvalue menyebabkan referensi penerusan menjadi _Ty& &&, yang kemudian disederhanakan menjadi referensi lvalue _Ty&.
  • Meneruskan rvalue menyebabkan referensi forwarding menjadi referensi rvalue _Ty&&.
  • Alasan referensi penerusan (seperti _Ty&&) ada bukan untuk optimasi, melainkan untuk menerima apa yang Anda teruskan kepadanya lalu meneruskannya secara transparan dan efisien. Anda biasanya hanya akan menjumpai referensi forwarding jika menulis (atau mempelajari secara mendalam) kode pustaka—misalnya, fungsi pabrik yang meneruskan argumen ke konstruktor.

Sources

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