Mengenal C++ Value Category
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.
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.