Pembagian lvalue dan rvalue pada chapter sebelumnya nyaman dipakai, tetapi standar C++ sejak C++17 membagi ekspresi lebih halus lagi menjadi tiga value category: lvalue, prvalue, dan xvalue. Pembagian ini bukan formalitas akademis — ketiganya punya peran berbeda dalam aturan pengikatan referensi dan move semantics yang akan kita pakai sepanjang section ini. Compiler mengetahui kategori setiap ekspresi secara pasti, dan kita bisa memintanya bersaksi lewat decltype beserta static_assert. Di chapter ini kita memetakan ketiga kategori ke bahasa sehari-hari, lalu membuktikan klasifikasi tiap contoh dengan kode yang diuji.

Tiga Kategori Resmi C++17 #

Standar C++17 menetapkan bahwa setiap ekspresi termasuk tepat satu dari tiga kategori berikut:

KategoriNama lengkapCiri utamaContoh
lvaluelocator valuepunya identitas dan alamat; bisa dipakai berulangvariabel s, s[0], *ptr, ++i
prvaluepure rvaluenilai murni sementara; akan berakhir di akhir pernyataan42, s + "!", buat(), i++
xvalueexpiring valueobjek yang umurnya mau habis karena ditandai untuk dipindahstd::move(s), static_cast<std::string&&>(s)

Dari tiga kategori ini terbentuk dua kelompok besar yang dipakai dalam istilah standar. glvalue (generalized lvalue) mencakup lvalue + xvalue: semua ekspresi yang punya identitas. rvalue mencakup prvalue + xvalue: semua ekspresi yang isinya boleh dipindahkan. Perhatikan bahwa xvalue masuk ke kedua kelompok — objek yang ditandai pindah tetap punya identitas, tetapi umurnya sudah dihitung mundur. Inilah yang membuat std::move begitu khas: ia tidak mengubah tipe apa pun, ia hanya mengubah kategori ekspresi menjadi xvalue.

Klasifikasi yang Dibuktikan lewat decltype #

decltype pada ekspresi tidak mengevaluasi ekspresinya — compiler hanya mencatat tipe hasilnya, termasuk kategori referensinya. Kita bisa menjadikannya alat klasifikasi yang tegas: setiap klaim kategori dijaga static_assert, sehingga bila klasifikasi kita salah, program bahkan tidak bisa dikompilasi.

cpp
// value-categories.cpp — prvalue, xvalue, lvalue: bukti lewat decltype
// Kompilasi: g++ -std=c++23 -Wall -Wextra value-categories.cpp -o value-categories

#include <print>
#include <string>
#include <type_traits>
#include <utility>

std::string buat() { return "hasil fungsi"; }  // ekspresi panggilan = prvalue

int main() {
    int i = 0;
    std::string s = "objek bernama";  // s: lvalue

    // decltype pada ekspresi tidak mengevaluasi ekspresinya, hanya mencatat tipe
    // hasilnya — termasuk kategori referensinya. Setiap klaim dijaga
    // static_assert, sehingga bila klasifikasi salah, program tidak dikompilasi.

    std::println("s             : lvalue  (decltype -> std::string)");
    static_assert(std::is_same_v<decltype(s), std::string>);

    std::println("s[0]          : lvalue  (decltype -> char&)");
    static_assert(std::is_same_v<decltype(s[0]), char&>);

    std::println("++i           : lvalue  (decltype -> int&)");
    static_assert(std::is_same_v<decltype(++i), int&>);

    std::println("buat()        : prvalue (decltype -> std::string)");
    static_assert(std::is_same_v<decltype(buat()), std::string>);

    std::println("i++           : prvalue (decltype -> int)");
    static_assert(std::is_same_v<decltype(i++), int>);

    std::println("s + \"!\"       : prvalue (decltype -> std::string)");
    static_assert(std::is_same_v<decltype(s + "!"), std::string>);

    std::println("std::move(s)  : xvalue  (decltype -> std::string&&)");
    static_assert(std::is_same_v<decltype(std::move(s)), std::string&&>);

    std::println("static_cast<std::string&&>(s) : xvalue (decltype -> std::string&&)");
    static_assert(std::is_same_v<decltype(static_cast<std::string&&>(s)), std::string&&>);
    return 0;
}

Output yang Dihasilkan:

text
s             : lvalue  (decltype -> std::string)
s[0]          : lvalue  (decltype -> char&)
++i           : lvalue  (decltype -> int&)
buat()        : prvalue (decltype -> std::string)
i++           : prvalue (decltype -> int)
s + "!"       : prvalue (decltype -> std::string)
std::move(s)  : xvalue  (decltype -> std::string&&)
static_cast<std::string&&>(s) : xvalue (decltype -> std::string&&)

Mari kita bedah:

  1. Baris 19-20 (decltype(s)): nama variabel adalah lvalue, dan decltype mengembalikan tipe deklarasinya std::string — tanpa &, karena decltype pada ekspresi tanpa tanda kurung mengikuti tipe yang dideklarasikan.
  2. Baris 22-23 dan 25-26 (s[0] dan ++i): operator[] milik std::string mengembalikan char& dan pre-increment mengembalikan int& — keduanya menghasilkan referensi ke objek yang bisa ditulis ulang, ciri khas lvalue.
  3. Baris 28-35 (buat(), i++, s + "!"): ketiganya prvalue. Post-increment mengembalikan salinan nilai lama (nilai murni), sementara pre-increment mengembalikan objek itu sendiri — perbedaan halus yang kini bisa kita baca dari kategorinya.
  4. Baris 37-41 (std::move dan static_cast): keduanya menghasilkan std::string&& — tanda xvalue. std::move(s) pada dasarnya adalah singkatan yang lebih terbaca dari static_cast<std::string&&>(s).
  5. Baris 20-41 (static_assert): delapan asersi jenis ini tidak punya biaya runtime; semuanya diperiksa saat kompilasi dan menjadi dokumentasi yang tidak bisa basi.

Pemetaan ke Istilah Sehari-hari #

Bila ketiga kategori masih terasa abstrak, pemetaan berikut biasanya menempel cepat. lvalue adalah objek dengan nama atau alamat yang jelas — kita bisa mengambil alamatnya, menulis kepadanya, dan memakainya lagi besok. prvalue adalah nilai mentah yang baru lahir — seperti hasil perhitungan yang langsung diserahkan ke penerima tanpa sempat diberi nama. xvalue adalah objek yang masih eksis tetapi sudah “dijadwalkan” untuk dipindahkan — biasanya karena kita sendiri yang menandainya lewat std::move.

Perbedaan prvalue dan xvalue paling terasa di bagian belakang section ini. Keduanya sama-sama rvalue, tetapi prvalue mendapat perlakuan istimewa: objek prvalue yang dikembalikan fungsi dijamin dikonstruksi langsung di tujuan (copy elision), tanpa copy dan tanpa move. Untuk xvalue, pindahan tetap mungkin terjadi. Detailnya menunggu kita sampai di chapter Copy Elision.

Dengan tiga kategori ini di tangan, istilah “rvalue reference” pada chapter berikutnya bisa dibaca persis sesuai artinya: referensi yang mengikat kelompok rvalue — yaitu prvalue dan xvalue.

Rujukan resmi: cppreference — value category , cppreference — decltype .