Kategori nilai

lvalue, rvalue, xvalue, dan referensi &&.

Daftar isi

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.

KategoriMemiliki identitasSumber dayanya boleh dialihkanContoh
lvalueyatidakvariabel teks, elemen v[0]
prvaluetidaktidak berlakustd::string("data"), 42
xvalueyayastd::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.

cpp kategori-nilai.cpp
#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\"))");
}
text Keluaran
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=false

Bagian 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.

cpp Contoh: std::move pada objek const
#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));
}
text Keluaran
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 teks tidak berubah ketika std::move(teks) ditulis. Yang berubah adalah kategori ekspresinya. Memahami perbedaan ini mencegah kekeliruan menilai bahwa std::move mengubah 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 overload T&& bila tipe argumennya mampu mengalihkan sumber dayanya.
  • Menganggap x dan (x) sama dalam decltype. Bentuk pertama membaca tipe deklarasi, sedangkan bentuk kedua menguji ekspresi lvalue sehingga menghasilkan T&. Perbedaan satu tanda kurung ini sering menghasilkan tipe yang tidak diduga.
  • Memanggil std::move pada objek const. Ekspresi hasilnya tetap const sehingga overload T&& tidak dapat terikat, dan pemilihan overload jatuh ke const 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.
  • decltype menerjemahkan kategori ekspresi menjadi T, T&, atau T&&, dan atribut seperti std::is_rvalue_reference memeriksanya saat kompilasi.

Referensi#