Section Modern C++ ditutup dengan janji bahwa detail teori di balik move semantics akan kita bongkar di sini. Janji itu dimulai dari dua kata yang paling sering dipakai dan paling sering disalahpahami di C++: lvalue dan rvalue. Setiap ekspresi dalam program C++ memiliki satu dari dua sifat dasar ini: ia menunjuk ke objek yang punya identitas dan alamat di memori, atau ia hanyalah nilai sementara yang akan berakhir begitu pernyataannya selesai. Kategori ekspresi bukan sekadar jargon buku teks — compiler memakainya untuk memutuskan referensi mana yang boleh terikat, fungsi overload mana yang dipilih, dan apakah objek boleh dipindahkan. Di chapter ini kita menetapkan definisi praktisnya, membuktikannya lewat operator &, dan melihat kategori menentukan pilihan overload secara nyata.

Definisi Praktis: Identitas versus Nilai #

Cara termudah membedakan keduanya adalah dengan dua pertanyaan. Pertama: apakah ekspresi itu menunjuk ke lokasi memori yang bisa kita sebut alamatnya? Kedua: apakah ekspresi itu masih hidup setelah pernyataan berakhir?

  • lvalue (locator value): ekspresi yang punya identitas — nama variabel, elemen array, anggota objek, atau hasil dereference pointer. Kita bisa mengambil alamatnya dengan &, dan lvalue bisa berdiri di sisi kiri operator penugasan.
  • rvalue: nilai sementara tanpa identitas — hasil a + b, pemanggilan fungsi yang mengembalikan objek by value, atau literal seperti 42. Ekspresi ini akan berakhir di akhir pernyataannya, dan kita tidak bisa mengambil alamatnya.

Satu contoh sehari-hari: pada std::string s = teks + "!";, s adalah lvalue karena s punya nama dan akan terus hidup, sedangkan teks + "!" adalah rvalue karena hasil gabungannya hanyalah nilai sementara yang langsung menyelesaikan tugasnya. Perhatikan bahwa kategori melekat pada ekspresi, bukan pada tipe: teks dan teks + "!" sama-sama bertipe std::string, tetapi kategorinya berbeda.

Membuktikan lewat Compiler dan Overload #

Klaim “lvalue bisa diambil alamatnya, rvalue tidak” bisa kita buktikan tanpa perlu berdebat: biarkan compiler yang bersaksi. Program berikut mengambil alamat sebuah lvalue, lalu memanggil dua overload yang berbeda supaya terlihat kategori apa yang compiler deteksi dari tiap argumen:

cpp
// lvalue-rvalue.cpp — perbedaan praktis lvalue dan rvalue
// Kompilasi: g++ -std=c++23 -Wall -Wextra lvalue-rvalue.cpp -o lvalue-rvalue

#include <print>
#include <string>

// Fungsi bantu: menerima alamat sebuah objek string.
// Bila &x bisa dihitung, pointer-nya sampai ke sini dengan nilai sah.
bool alamatDapatDiambil(const std::string* p) {
    return p != nullptr;
}

// Dua overload dengan kategori referensi berbeda.
// Compiler memilih berdasarkan kategori ekspresi argumen.
void simpan(std::string& s) {            // lvalue reference: hanya lvalue
    std::println("  simpan(std::string&)  dipilih -> lvalue: '{}'", s);
}

void simpan(std::string&& s) {           // rvalue reference: rvalue
    std::println("  simpan(std::string&&) dipilih -> rvalue: '{}'", s);
}

int main() {
    std::string teks = "hobi";  // teks: lvalue (punya nama, punya alamat)

    std::println("teks       : lvalue, &teks bisa dihitung -> {}",
                 alamatDapatDiambil(&teks));
    std::println("teks + \"!\" : rvalue, hasil gabungan sementara tanpa nama");

    // kategori ekspresi menentukan overload yang dipilih:
    simpan(teks);                       // lvalue -> std::string&
    simpan(teks + "!");                 // rvalue -> std::string&&
    simpan(std::string("sementara"));   // prvalue -> std::string&&
    return 0;
}

Output yang Dihasilkan:

text
teks       : lvalue, &teks bisa dihitung -> true
teks + "!" : rvalue, hasil gabungan sementara tanpa nama
  simpan(std::string&)  dipilih -> lvalue: 'hobi'
  simpan(std::string&&) dipilih -> rvalue: 'hobi!'
  simpan(std::string&&) dipilih -> rvalue: 'sementara'

Mari kita bedah:

  1. Baris 9-11 (alamatDapatDiambil): fungsi bantu ini menerima pointer dan memeriksa apakah pointer-nya sah. Kita memakai perantara ini (bukan membandingkan langsung) supaya pemeriksaan alamatnya nyata dan tidak memicu warning “alamat tidak akan pernah NULL”.
  2. Baris 26-27 (mengambil alamat lvalue): &teks dihitung compiler tanpa protes, dan nilainya true — bukti pertama bahwa teks adalah lvalue yang punya alamat.
  3. Baris 15-17 dan 19-21 (dua overload): simpan(std::string&) hanya menerima lvalue, sedangkan simpan(std::string&&) hanya menerima rvalue. Overload resolution menjadi alat ukur kategori yang sangat peka.
  4. Baris 31-33 (tiga pemanggilan): simpan(teks) memilih versi & karena teks lvalue; simpan(teks + "!") memilih versi && karena hasil penggabungan string adalah rvalue; simpan(std::string("sementara")) juga memilih versi && karena objek yang dibuat langsung dari constructor adalah rvalue.

Sekarang bagian sebaliknya: apa yang terjadi bila kita memaksa mengambil alamat sebuah rvalue? Kita coba di file terpisah dan biarkan compiler menolaknya:

cpp mencoba mengambil alamat rvalue (tidak dikompilasi)
const std::string* p = &(teks + "!");   // rvalue tidak punya alamat

Pesan error yang dihasilkan compiler:

text
ch1-addr.cpp: In function ‘int main()’:
ch1-addr.cpp:5:35: error: taking address of rvalue [-fpermissive]
    5 |     const std::string* p = &(teks + "!");   // gagal: rvalue tidak punya alamat
      |                             ~~~~~~^~~~~~

Error taking address of rvalue adalah keputusan yang disengaja oleh desain bahasa. Nilai sementara tidak punya lokasi yang bisa dijanjikan tetap ada — pada baris berikutnya ia sudah tidak terjamin hidup — sehingga menyerahkan alamatnya akan menghasilkan pointer menunjuk ke ruang kosong. Compiler memutuskan kesalahan ini lebih awal, tepat saat kompilasi.

Mengapa Kategori Ini Penting #

Dua pembuktian di atas bukan sekadar curiosity. Kategori ekspresi adalah sinyal yang dipakai compiler untuk menentukan pilihan penting dalam program kita. Ketika argumen adalah rvalue, compiler tahu objek itu sebentar lagi berakhir, sehingga isinya boleh diambil alih — inilah pintu masuk move semantics yang menjadi benang merah section ini. Ketika argumen adalah lvalue yang masih dipakai, isi objek harus dijaga. Sama pentingnya: untuk class yang kita tulis sendiri, overload const T& dan T&& memungkinkan kita memperlakukan kedua jenis pemanggil secara berbeda — baca-saja untuk lvalue, ambil-alih untuk rvalue.

Dengan kata lain, semua chapter berikutnya — rvalue reference, move constructor, std::move, hingga perfect forwarding — dibangun di atas kemampuan kita membaca kategori ekspresi dengan benar. Karena itu juga, pembagian dua kategori di atas masih belum lengkap: standar C++17 membaginya lebih halus lagi menjadi tiga kategori resmi, dan itulah topik chapter berikutnya.

Rujukan resmi: cppreference — value category .