Setiap ekspresi dalam C++ memiliki kategori nilai (value category), yaitu label yang menentukan bagaimana ekspresi itu boleh dipakai, mulai dari pengambilan alamatnya sampai kebolehan mengalihkan sumber dayanya. Topik ini membahas tiga kategori dasar, yaitu lvalue, prvalue, dan xvalue, hubungannya dengan referensi & dan &&, serta cara compiler memilih overload berdasarkan kategori tersebut. Setelah topik ini, notasi T&&, std::move, dan decltype dapat dibaca dengan tepat.
Tiga kategori dasar#
Ekspresi yang menunjuk objek dengan identitas disebut lvalue, yakni ekspresi yang alamatnya dapat diambil dan biasanya dijangkau lewat sebuah nama. Ekspresi yang menghasilkan nilai untuk menginisialisasi objek tanpa membawa identitas disebut prvalue, contohnya literal 42 atau pemanggilan std::string("data"). Ekspresi yang memiliki identitas sekaligus menandai bahwa sumber dayanya boleh dialihkan disebut xvalue (expiring value), contohnya std::move(teks). Tabel berikut merangkum ciri ketiganya.
| Kategori | Memiliki identitas | Sumber dayanya boleh dialihkan | Contoh |
|---|---|---|---|
| lvalue | ya | tidak | variabel teks, elemen v[0] |
| prvalue | tidak | tidak berlaku | std::string("data"), 42 |
| xvalue | ya | ya | std::move(teks) |
Dokumentasi resmi juga memakai dua istilah pengelompokan. glvalue mencakup lvalue dan xvalue, sedangkan rvalue mencakup prvalue dan xvalue, sehingga istilah lama “rvalue” kini berarti segala yang boleh dialihkan. Dua istilah ini berguna saat membaca halaman rujukan, sedangkan analisis kode cukup berpijak pada tiga kategori dasarnya.
Pembedaan yang paling penting: kategori menempel pada ekspresi, bukan pada objeknya. Objek yang sama muncul sebagai lvalue ketika namanya ditulis dan sebagai xvalue ketika dibungkus std::move. Pemilihan overload dan kebolehan pengalihan dinilai dari kategori ekspresi argumen, sehingga pembedaan ini berdampak langsung pada kode yang dijalankan.
Referensi & dan referensi &&#
Tanda & pada tipe membentuk referensi lvalue, yaitu nama alias yang hanya terikat pada ekspresi lvalue. Tanda && membentuk referensi rvalue, yaitu referensi yang dirancang untuk terikat pada prvalue dan xvalue. Referensi rvalue inilah yang membuat sebuah fungsi mampu membedakan argumen yang masih dibutuhkan pemanggilnya dan argumen yang sudah boleh dikosongkan. Bentuk const T& berada di antara keduanya, karena referensi tersebut terikat pada lvalue maupun prvalue dan memperpanjang masa hidup sementara yang diikatnya.
Program berikut menguji pemilihan overload dengan dua fungsi bernama sama, lalu membaca kategori beberapa ekspresi lewat decltype. Kedua pemeriksaan menggunakan pustaka standar saja sehingga dapat dijalankan sebagai satu berkas.
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
void terima(const std::string& nilai) {
std::cout << " dipilih: terima(const std::string&)\n";
}
void terima(std::string&& nilai) {
std::cout << " dipilih: terima(std::string&&)\n";
}
template <typename T>
void tampilkan(const char* label) {
std::cout << label << " -> "
<< "is_lvalue_reference="
<< (std::is_lvalue_reference<T>::value ? "true" : "false")
<< ", is_rvalue_reference="
<< (std::is_rvalue_reference<T>::value ? "true" : "false")
<< '\n';
}
int main() {
std::string teks = "data";
std::cout << "pemilihan overload:\n";
std::cout << "argumen teks (lvalue):\n";
terima(teks);
std::cout << "argumen std::string(\"data\") (prvalue):\n";
terima(std::string("data"));
std::cout << "argumen std::move(teks) (xvalue):\n";
terima(std::move(teks));
std::string lain = "data";
std::cout << "kategori melalui decltype:\n";
tampilkan<decltype(lain)>("decltype(lain)");
tampilkan<decltype((lain))>("decltype((lain))");
tampilkan<decltype(std::move(lain))>("decltype(std::move(lain))");
tampilkan<decltype(std::string("data"))>("decltype(std::string(\"data\"))");
}pemilihan overload:
argumen teks (lvalue):
dipilih: terima(const std::string&)
argumen std::string("data") (prvalue):
dipilih: terima(std::string&&)
argumen std::move(teks) (xvalue):
dipilih: terima(std::string&&)
kategori melalui decltype:
decltype(lain) -> is_lvalue_reference=false, is_rvalue_reference=false
decltype((lain)) -> is_lvalue_reference=true, is_rvalue_reference=false
decltype(std::move(lain)) -> is_lvalue_reference=false, is_rvalue_reference=true
decltype(std::string("data")) -> is_lvalue_reference=false, is_rvalue_reference=falseBagian pertama keluaran memperlihatkan aturan pemilihannya. Argumen teks berkategori lvalue, sehingga overload yang menerima const std::string& dipilih. Prvalue dan xvalue justru memilih overload std::string&&, karena kedua overload sama-sama dapat menerima argumen rvalue tetapi versi && lebih spesifik. Pola inilah yang dipakai pustaka standar untuk membedakan penyalinan dan pengalihan pada container.
Membaca kategori dengan decltype#
Bagian kedua keluaran mengikuti aturan decltype pada ekspresi. Tanpa tanda kurung, decltype(lain) membaca tipe deklarasi variabelnya, yaitu std::string polos. Dengan tanda kurung ganda, decltype((lain)) menguji ekspresi yang berkategori lvalue, sehingga hasilnya menjadi std::string& dan std::is_lvalue_reference melaporkan true. Ekspresi std::move(lain) berkategori xvalue sehingga hasilnya std::string&&. Prvalue menghasilkan std::string polos, sehingga kedua atribut melaporkan false.
Atribut tipe std::is_lvalue_reference dan std::is_rvalue_reference mengubah pertanyaan kategori menjadi nilai bool saat kompilasi, tanpa mengeksekusi apa pun. Kombinasi decltype dan atribut ini adalah alat baku untuk memeriksa pekerjaan kategori nilai, dan teknik yang sama menjadi dasar pemeriksaan pada topik perfect forwarding.
Pengaruh const pada pemilihan overload#
Kualifikasi const ikut menentukan hasil pemilihan overload. Pada objek const std::string, pemanggilan std::move(teks) menghasilkan xvalue yang tetap const, dan xvalue itu tidak dapat terikat pada parameter std::string&& karena parameter tersebut berhak mengubah sumber yang diikatnya. Pemilihan overload kemudian jatuh ke satu-satunya kandidat yang tersisa, yaitu const std::string&, sehingga salinan yang sebenarnya ingin dihindari justru terjadi tanpa pesan apa pun dari compiler.
Program berikut memanggil kedua overload yang sama dengan satu argumen const yang dibungkus std::move.
#include <iostream>
#include <string>
#include <utility>
void terima(const std::string& nilai) {
std::cout << " dipilih: terima(const std::string&)\n";
}
void terima(std::string&& nilai) {
std::cout << " dipilih: terima(std::string&&)\n";
}
int main() {
const std::string teks = "data";
std::cout << "argumen const std::move(teks):\n";
terima(std::move(teks));
}argumen const std::move(teks):
dipilih: terima(const std::string&)Overload penyalin tetap terpilih meskipun pemanggilannya ditandai std::move. Penanda std::move hanya menyatakan bahwa sumbernya boleh dialihkan, sedangkan const pada objeknya menghalangi pengalihan itu karena operasi pindah membutuhkan hak menulis untuk mengosongkan sumbernya. Kebiasaan yang baik adalah memanggil std::move hanya pada objek non-const, dan kecurigaan terhadap penyalinan yang tak terduga dapat diuji dengan kombinasi decltype dan atribut tipe seperti pada bagian ini.
Kesalahan umum#
- Menganggap kategori menempel pada objek. Objek
tekstidak berubah ketikastd::move(teks)ditulis. Yang berubah adalah kategori ekspresinya. Memahami perbedaan ini mencegah kekeliruan menilai bahwastd::movemengubah objeknya. - Memilih
const T&untuk seluruh overload. Bentuk ini menerima semua argumen, tetapi sumber daya argumen rvalue tidak pernah dialihkan dan penerima terpaksa menyalinnya. Tambahkan overloadT&&bila tipe argumennya mampu mengalihkan sumber dayanya. - Menganggap
xdan(x)sama dalamdecltype. Bentuk pertama membaca tipe deklarasi, sedangkan bentuk kedua menguji ekspresi lvalue sehingga menghasilkanT&. Perbedaan satu tanda kurung ini sering menghasilkan tipe yang tidak diduga. - Memanggil
std::movepada objek const. Ekspresi hasilnya tetap const sehingga overloadT&&tidak dapat terikat, dan pemilihan overload jatuh keconst T&tanpa pesan apa pun. Operasi pindah hanya ditujukan bagi objek non-const, karena mengosongkan sumbernya membutuhkan hak menulis.
Ringkasan#
- Kategori nilai menempel pada ekspresi, bukan pada objeknya: lvalue memiliki identitas, prvalue adalah nilai untuk inisialisasi, dan xvalue memiliki identitas sekaligus boleh dialihkan.
- Referensi
&hanya terikat pada lvalue, sedangkan&&terikat pada prvalue dan xvalue sehingga overload&&dipilih untuk argumen sementara. decltypemenerjemahkan kategori ekspresi menjadiT,T&, atauT&&, dan atribut sepertistd::is_rvalue_referencememeriksanya saat kompilasi.
Referensi#
- Value categories — cppreference.com — Definisi resmi lvalue, prvalue, xvalue, beserta kelompok glvalue dan rvalue.
- Reference declaration — cppreference.com — Aturan pengikatan referensi lvalue dan rvalue beserta pengecualiannya.
- Lvalue references — LearnCpp — Perilaku referensi lvalue dan pengikatan const pada referensi.
- Rvalue references — LearnCpp — Pembahasan referensi rvalue dan pemanfaatannya pada overload.