Mengenal C++ Value Category

Januar Andaria
Nodeflux
Published in
6 min readSep 13, 2019

Setiap ekspresi dalam C++ memiliki tipe dan value category. C++17 memiliki 5 value categories yaitu glvalue, lvalue, xvalue, rvalue and prvalue. Salah satu hal penting yang perlu diketahui adalah istilah lvalue dan rvalue tidak hanya ada pada value category, tetapi juga ada pada references. Hal tersebut bisa membingungkan dalam mempelajari value category ataupun tipe references. Sebagai contoh pada ekspresi int&& foo = 24;, variabel foo memiliki tipe rvalue references (int&&), tetapi ekspresi nama foo memiliki tipe ekspresi lvalue reference (int&) dan value categoty-nya adalah lvalue. Taksonomi penamaan value category dapat dilihat pada gambar dibawah ini.

Taksonomi value category
Gambar 1. Taksonomi value category

Suatu ekspresi hanya memiliki satu value category antara lvalue, xvalue atau prvalue. Dalam sejarah perkembangan C++, value category juga mengalami perubahan definisi dari satu versi C++ ke versi lainnya. Untuk memahami arti value category C++17, ada baiknya diketahui arti value category pada versi C dan C++ sebelumnya.

K&R C

Istilah value category sudah ada dari bahasa C (K&R C), pada awalnya value category hanya dibagi 2, yaitu lvalue dan rvalue. Nama value category tersebut didasarkan dari penempatan nilai terhadap sisi assigment operator =. Nilai yang bisa berada di sisi kiri assigment operator disebut lvalue sedangkan di sisi kanan disebut rvalue. Akan tetapi, kenyataannya lvalue bisa juga berada sisi kanan assigment operator sedangkan rvalue hanya bisa pada sisi kanan. Hal ini membuat lvalue memiliki arti "left value" dan rvalue sebagain "right value". Contoh lvalue dan rvalue dapat dilihat pada kode dibawah ini.

int i;
i = 24; // OK
24 = i; // ERROR

Pada contoh diatas, nama i adalah lvalue sedangkan literal 24 adalah rvalue, menempatkan literal 24 pada sisi kiri assigment operator adalah error. Selain penempatannya, lvalue dan rvalue juga memiliki karakteristik lain, salah satunya adalah lvalue dapat diakses alamat memorinya. Pada contoh dibawah ini, nama i adalah lvalue yang bisa diakses alamat memorinya, sedangkan literal 24 tidak bisa.

int *p = &i; // OK
int *q = &24; // ERROR

C89

Selanjutnya, bahasa C distandarisasi dan dikenal dengan C89 atau ANSI C. Istilah value category mengalami perubahan pada versi C ini. Pada C89 terdapat fitur baru yaitu const yang membuat lvalue tidak bisa berada pada sisi kiri assigment operator setelah inisiasi.

const int c = 0;
c = 1; // ERROR

Hal ini membuat definisi lvalue diubah menjadi “locator value” daripada “left value”.

C++11

C++11 memiliki sematik baru yaitu move semantic. Semantik tersebut menjadi bagian dari dua properti independen baru pada suatu ekspresi, yaitu:

  • Memiliki identitas (has identity), secara garis besar adalah ekspresi yang mengacu pada suatu objek, seperti references atau nama variabel; dan
  • Bisa dipindah dari (can be moved from), adalah ekspresi yang dapat dilakukan move semantics, seperti objek temporary.

Berdasarkan properti ekspresi tersebut, value category didefinisikan ulang menjadi:

  • glvalue adalah ekspresi yang memiliki identitas
  • rvalue adalah ekspresi yang bisa dipindah dari
  • lvalue adalah ekspresi yang memiliki identitas dan tidak bisa dipindah dari
  • prvalue adalah ekspresi yang bisa dipindah dari dan tidak memiliki identitas
  • xvalue adalah ekspresi yang memiliki identitas dan bisa dipindah dari

Taksonomi value category C++11 dapat dilihat pada Gambar 1.

// move semantic
void move_from(int&& arg);
//...int i = 24;
move_from(i); // ERROR
move_from(24); // OK
move_from(std::move(i)); // OK

Pada contoh diatas, value category ekspresi nama i adalah lvalue, literal 24 adalah rvalue dan std::move(i) adalah xvalue. Ekspresi literal 24 dan std::move(i) juga adalah rvalue (sesuai taksonomi) karena keduanya dapat dipindah dalam move semantic. Ekspresi nama i memiliki identitas karena variabel i mengacu pada suatu objek integer. Ekspresi std::move(i) juga memiliki identitas yaitu nilai yang return dari fungsi std::move adalah reference yang mengacu pada objek yang sama dengan objek yang diacu oleh variabal i dalam argumennya.

C++17

Value category pada C++17 mengalami perubahan terutama pada prvalue. Ekspresi prvalue adalah ekspresi yang melakukan inisiasi, dan tidak “bisa dipindah dari” secara langsung. Untuk lebih jelasnya, perhatikan contoh ekspresi dibawah ini.

T var = T();

Contoh diatas adalah copy initialization. Pada C++ sebelum C++11, T() adalah rvalue dan akan melakukan copy constructor ke variabel var. Pada C++11, T() adalah prvalue (secara taksonomi masih rvalue) dan akan melakukan move constructor ke variabel var. Jika move constructor dihapus, maka ekspresi tersebut akan error karena mencoba mengakses move constructor yang telah dihapus. Pada C++17, T() adalah prvalue dan akan di- passing langsung ke variabel var tanpa menlakukan move contructor atau copy contructor, sehingga ekspresi tersebut ekuivalen dengaan ekspresi

T var;

Konversi menjadi xvalue

Suatu prvalue dapat dikonversi menjadi xvalue. Konversi terjadi dengan menginisiasi objek sementara dengan tipe dari prvalue yang menghasilkan xvalue yang mengacu pada objek sementara tersebut. Kode dibawah ini adalah contoh “materialization” prvalue menjadi xvalue.

struct S { int m; };
int i = S{}.m; // prvalue S{} "materialize" menjadi xvalue

Akses member variable .m hanya dilakukan pada glvalue, sehingga objek sementara xvalue dibuat dari prvalue S{}.

Tipe ekspresi dan decltype((expr))

Value category suatu ekspresi dapat diketahui dari tipe hasil evaluasi decltype((expr)) (tanda kurung tambahan perlu, bukan decltype(expr)). Jika tipe hasil decltype((expr)) adalah lvalue reference maka ekspresinya adalah lvalue, tipe rvalue reference untuk ekspresinya xvalue dan tipe tanpa reference untuk prvalue. Kode dibawah ini adalah contoh program sederhana menggunakan decltype((expr)) untuk mengetahui value category suatu ekspresi (https://godbolt.org/z/6oEcPb).

#include <iostream>template <class T>
struct val_cat { auto operator()() { return "prvalue"; } };
template <class T>
struct val_cat<T&> { auto operator()() { return "lvalue"; } };
template <class T>
struct val_cat<T&&> { auto operator()() { return "xvalue"; } };
#define PRINT_VAL_CAT(expr) \
std::cout << val_cat<decltype((expr))>{}() << '\n'
auto main(int, char*[]) -> int
{
struct S { int i{}; };
int i = 24; PRINT_VAL_CAT(i); // lvalue
PRINT_VAL_CAT(1); // prvalue
PRINT_VAL_CAT(std::move(i)); // xvalue
PRINT_VAL_CAT(S{0}); // prvalue
PRINT_VAL_CAT(S{0}.i); // xvalue
return 0;
}

Copy elision dan RVO

Copy elision adalah suatu teknik optimisasi yang dilakukan kompiler untuk menghindari copying (atau moving) objek yang tidak perlu. Variasi dari copy elision pada objek sementara dari return statement suatu fungsi dikenal sebagai RVO, “ return value optimization”. Sebagai contoh, perhatikan program dibawah ini.

#include <iostream>auto main(int, char*[]) -> int {
struct Foo {
Foo() { std::cout << "Default Conctructor\n"; }
Foo(const Foo&) { std::cout << "Copy Conctructor\n"; }
Foo(Foo&&) { std::cout << "Move Conctructor\n"; }
};
Foo var = Foo{};
return 0;
}

Jika tanpa copy elision (disable optimisasai copy elision dapat dilakukan dengan menggunakan flag -fno-elide-constructors pada GCC dan Clang), output program diatas adalah sebagai berikut

Default Conctructor
Move Conctructor

Hal ini terjadi karena ekspresi Foo{} melakukan default constructor terlebih dahulu lalu objek sementara yang di- construct akan dipindah ke variabel var sehingga terjadi move constructor. Optimisasi copy elision pada program diatas akan menghasilkan ouput sebagai berikut

Default Conctructor

Dari hasil diatas, tidak terjadi move constructor karena kompiler menghilangkan (elide) peng- copy-an yang tidak perlu.

Guaranteed copy elision

Pada versi sebelum C++17, copy elision hanya dapat terjadi jika objek dapat di- copy atau move, karena copy elision adalah bentuk optimisasi. Jika move constructor dihapus, maka copy elision tidak dapat dilakukan walaupun move constructor sebenarnya tidak dibutuhkan. Sebagain contoh, program dibawah ini akan error pada C++14.

#include <iostream>auto main(int, char*[]) -> int {
struct Foo {
Foo() { std::cout << "Default Conctructor\n"; }
Foo(const Foo&) = delete; // copy constructor dihapus
Foo(Foo&&) = delete; // move constructor dihapus
};
Foo var = Foo{}; // ERROR

return 0;
}

Masalah diatas diselesaikan dengan perubahan model prvalue pada C++17 sehingga ekspresi Foo{} langsung di- passing ke variabel var. Model prvalue ini juga membuat RVO pasti terjadi pada C++17.

Value categoty dan references

Seperti yang telah disebutkan diatas, istilah lvalue dan rvalue juga dipakai dalam references. Hal tersebut dapat membingungkan dalam menggunakan references. Sebagai contoh, perhatikan kode di bawah ini.

void foo(int&& i) {
std::cout << "Hello int " << i << "\n";
}
void bar(int&& j) {
foo(j); // ERROR: j adalah lvalue
}

Ekspresi pemanggilan fungsi foo(j) akan error karena ekspresi nama j adalah lvalue dan tipe ekspresinya adalah lvalue references (int& tipe dari decltype((j))). Jika ingin mem- passing nama j sebagai rvalue reference, maka ekspresinya perlu diubah menjadi xvalue dengan menggunakan std::move(j) atau static_cast<int&&>(j) seperti pada contoh dibawah ini

void foo(int&& i) {
std::cout << "Hello int " << i << "\n";
}
void bar(int&& j) {
foo(std::move(j)); // OK: std::move(j) adalah xvalue
}

Kesimpulan

Ekspresi dalam C++ dibagi menjadi 5 value categories yang saling tumpang tindih. glvalue adalah ekspresi yang mengacu pada suatu objek, memiliki identitas. prvalue digunakan untuk inisiasi, tidak mengacu pada objek walaupun dapat “materialize” menjadi objek jika dibutuhkan. xvalue adalah glvalue yang bisa dipindah dari. lvalue adalah glvalue yang bukan xvalue. rvalue adalah value category yang bukan lvalue.

Definisi value category juga dapat menjadi bahan pertimbangan untuk mengadopsi penggunaan C++ versi terbaru. Seperti halnya value category pada C++17 yang menjamin terjadinya copy elision dan RVO sehingga dapat meningkatkan performa. Selain itu, terdapat fitur-fitur baru lainnya pada C++17 yang dapat memberikan manfaat dalam pengembangan software. Di Nodeflux, sebagai perusahaan Vision Artificial Intelligent pertama dan terbesar di Indonesia, selalu mengadopsi teknologi terbaru untuk pengembangan software termasuk menggunakan C++17 dengan segala benefitnya.

--

--