Kategori ekspresi, seperti nilai dan nilai , lebih banyak berhubungan dengan konsep-konsep teoretis dasar C ++ daripada aspek praktis penggunaannya. Untuk alasan ini, banyak programmer bahkan berpengalaman memiliki gagasan yang kabur tentang apa yang mereka maksudkan. Dalam artikel ini saya akan mencoba menjelaskan arti dari istilah-istilah ini sesederhana mungkin, menipiskan teori dengan contoh-contoh praktis. Saya akan segera melakukan reservasi: artikel ini tidak berpura-pura memberikan deskripsi yang paling lengkap dan ketat tentang kategori ekspresi, untuk detail saya sarankan menghubungi sumber secara langsung: standar bahasa C ++.
Artikel ini akan mengandung cukup banyak istilah bahasa Inggris, ini disebabkan oleh kenyataan bahwa beberapa dari mereka sulit diterjemahkan ke dalam bahasa Rusia, sementara yang lain diterjemahkan dalam sumber yang berbeda dengan cara yang berbeda. Oleh karena itu, saya akan sering menunjukkan istilah-istilah bahasa Inggris, menyoroti mereka dalam huruf miring .
Sedikit sejarah
Istilah lvalue dan rvalue muncul kembali dalam C. Perlu dicatat bahwa kebingungan diletakkan pada terminologi awalnya, karena mereka merujuk pada ekspresi, dan bukan pada nilai. Secara historis, nilai adalah apa yang bisa tersisa dari operator penugasan, dan nilai adalah apa yang hanya bisa benar .
lvalue = rvalue;
Namun, definisi semacam itu agak menyederhanakan dan mengubah esensi. Standar C89 mendefinisikan nilai sebagai pelacak objek , mis. Objek dengan lokasi memori yang dapat diidentifikasi. Dengan demikian, segala sesuatu yang tidak sesuai dengan definisi ini dimasukkan dalam kategori nilai.
Bjarn bergegas untuk menyelamatkan
Dalam C ++, terminologi kategori ekspresi telah berkembang cukup kuat, terutama setelah adopsi Standar C ++ 11, yang memperkenalkan konsep tautan nilai dan memindahkan semantik . Sejarah munculnya terminologi baru secara menarik dijelaskan dalam artikel Straustrup's "New" Value Terminology .
Terminologi baru dan lebih ketat didasarkan pada 2 properti:
- keberadaan identitas ( identitas ) - yaitu, beberapa parameter yang dapat dipahami apakah dua ekspresi merujuk ke entitas yang sama atau tidak (misalnya, alamat dalam memori);
- kemampuan untuk bergerak ( dapat dipindahkan dari ) - mendukung semantik gerakan.
Ekspresi yang mengekspresikan identitas digeneralisasi di bawah istilah glvalue ( nilai yang digeneralisasi ), ekspresi roaming disebut rvalue . Kombinasi dari dua properti ini telah mengidentifikasi 3 kategori utama ekspresi:
| Punya identitas | Tanpa identitas |
---|
Tidak bisa dipindahkan | lvalue | - |
Bisa dipindahkan | nilai x | nilai awal |
Bahkan, Standar C ++ 17 memperkenalkan konsep penyalinan salinan - situasi formalisasi di mana kompiler dapat dan harus menghindari menyalin dan memindahkan objek. Dalam hal ini, nilai awal mungkin tidak perlu dipindahkan. Detail dan contoh dapat ditemukan di sini . Namun, ini tidak mempengaruhi pemahaman tentang skema umum kategori ekspresi.
Dalam Standar C ++ modern, struktur kategori disajikan dalam bentuk skema seperti itu:

Mari kita periksa secara umum sifat-sifat kategori, serta ekspresi bahasa yang termasuk dalam masing-masing kategori. Saya segera mencatat bahwa daftar ekspresi di bawah ini untuk setiap kategori tidak dapat dianggap lengkap, untuk informasi yang lebih akurat dan terperinci, lihat langsung ke Standar C ++.
nilai glv
Ekspresi dalam kategori glvalue memiliki properti berikut:
- dapat secara implisit dikonversi ke nilai awal ;
- dapat bersifat polimorfik, yaitu, bagi mereka konsep tipe statis dan dinamis masuk akal;
- tidak dapat bertipe void - ini secara langsung mengikuti dari properti yang memiliki identitas, karena untuk ekspresi tipe batal tidak ada parameter yang akan membedakannya satu sama lain;
- dapat memiliki jenis yang tidak lengkap , misalnya, dalam bentuk pernyataan maju (jika diizinkan untuk ekspresi tertentu).
nilai
Ekspresi dalam kategori nilai memiliki properti berikut:
- Anda tidak bisa mendapatkan alamat nilai di memori - ini secara langsung mengikuti dari kurangnya properti identitas;
- tidak dapat berada di sebelah kiri pernyataan penugasan atau gabungan tugas;
- dapat digunakan untuk menginisialisasi tautan nilai konstan atau tautan nilai, sedangkan masa pakai objek meluas hingga masa pakai tautan;
- jika digunakan sebagai argumen ketika memanggil fungsi yang memiliki 2 versi overload: satu menerima tautan nilai konstan dan yang lainnya tautan nilai, maka versi yang menerima tautan nilai dipilih. Properti inilah yang digunakan untuk mengimplementasikan semantik gerakan :
class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); // A(const A&) A c(std::move(a)); // A(A&&)
Secara teknis, A&& adalah nilai tambah dan dapat digunakan untuk menginisialisasi referensi nilai konstan dan referensi nilai. Namun berkat properti ini, tidak ada ambiguitas, opsi konstruktor diterima yang menerima referensi nilai.
lvalue
Properti:
- semua properti glvalue (lihat di atas);
- Anda dapat mengambil alamatnya (menggunakan operator unary bawaan
&
); - nilai yang dapat dimodifikasi dapat berada di sisi kiri operator penugasan atau operator penugasan majemuk;
- dapat digunakan untuk menginisialisasi referensi ke nilai (baik konstan dan non-konstan).
Ekspresi berikut termasuk dalam kategori nilai :
- nama variabel, fungsi, atau bidang kelas jenis apa pun. Bahkan jika variabel adalah referensi nilai, nama variabel ini dalam ekspresi adalah nilai ;
void func() {} ......... auto* func_ptr = &func; // : auto& func_ref = func; // : int&& rrn = int(123); auto* pn = &rrn; // : auto& rn = rrn; // : lvalue-
- memanggil fungsi atau operator kelebihan beban yang mengembalikan referensi nilai , atau ekspresi konversi ke jenis referensi nilai ;
- operator penugasan built-in, operator penugasan majemuk (
=
, +=
, /=
, dll.), pra-kenaikan dan pra-penambah bawaan ( ++a
, --b
), operator dereference pointer bawaan ( *p
); - built-in operator akses oleh indeks (
a[n]
atau n[a]
), ketika salah satu operan adalah lvalue array; - memanggil fungsi atau pernyataan kelebihan beban yang mengembalikan referensi nilai ke fungsi;
- string literal seperti
"Hello, world!"
.
Sebuah string literal berbeda dari semua literal lainnya dalam C ++ tepatnya dalam arti bahwa itu adalah nilai (meskipun tidak dapat diubah). Misalnya, Anda bisa mendapatkan alamatnya:
auto* p = &βHello, world!β; // ,
nilai awal
Properti:
- semua nilai properti (lihat di atas);
- tidak boleh polimorfik: tipe ekspresi statis dan dinamis selalu bertepatan;
- tidak boleh dari tipe yang tidak lengkap (kecuali untuk tipe void , ini akan dibahas di bawah);
- tidak dapat memiliki tipe abstrak atau menjadi array elemen dari tipe abstrak.
Ekspresi berikut termasuk dalam kategori nilai awal :
- literal (kecuali string), misalnya
42
, true
atau nullptr
; - panggilan fungsi atau operator kelebihan beban yang mengembalikan non-referensi (
str.substr(1, 2)
, str1 + str2
, it++
) atau ekspresi konversi ke tipe non-referensi (misalnya, static_cast<double>(x)
, std::string{}
, (int)42
); - built-in post-increment dan post-decrement (
a++
, b--
), operasi matematika built-in ( a + b
, a % b
, a & b
, a << b
, dll.), operasi logis bawaan ( a && b
, a || b
!a
, dll.), operasi perbandingan ( a < b
, a == b
, a >= b
, dll.), operasi bawaan untuk mengambil alamat ( &a
); - penunjuk ini ;
- daftar barang;
- parameter template atipikal, jika bukan kelas;
- ekspresi lambda, misalnya
[](int x){ return x * x; }
[](int x){ return x * x; }
.
nilai x
Properti:
- semua nilai properti (lihat di atas);
- semua properti glvalue (lihat di atas).
Contoh ekspresi xvalue :
- memanggil fungsi atau operator bawaan yang mengembalikan referensi nilai, misalnya std :: move (x) ;
dan pada kenyataannya, untuk hasil memanggil std :: move (), Anda tidak bisa mendapatkan alamat di memori atau menginisialisasi tautan ke sana, tetapi pada saat yang sama, ungkapan ini bisa berupa polimorfik:
struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); // auto& r = std::move(xa); // std::move(xa).f(); // βXB::f()β
- built-in operator akses oleh indeks (
a[n]
atau n[a]
) ketika salah satu operan adalah array nilai.
Beberapa kasus khusus
Operator koma
Untuk operator koma bawaan, kategori ekspresi selalu cocok dengan kategori ekspresi operan kedua.
int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue
Ekspresi kosong
Panggilan ke fungsi yang mengembalikan void , mengetik ekspresi konversi menjadi void , dan melempar pengecualian dianggap sebagai ekspresi nilai awal, tetapi mereka tidak dapat digunakan untuk menginisialisasi referensi atau sebagai argumen untuk fungsi.
Operator perbandingan ternary
Definisi kategori ekspresi a ? b : c
a ? b : c
- kasusnya nontrivial, semuanya tergantung pada kategori argumen kedua dan ketiga ( b
dan c
):
- jika
b
atau c
bertipe batal , maka kategori dan tipe seluruh ekspresi berhubungan dengan kategori dan tipe argumen lainnya. Jika kedua argumen bertipe batal , maka hasilnya adalah nilai awal dari tipe batal ; - jika
b
dan c
adalah glvalue dari tipe yang sama, maka hasilnya adalah glvalue dari tipe yang sama; - dalam kasus lain, hasilnya adalah nilai awal.
Untuk operator ternary, sejumlah aturan didefinisikan sesuai dengan konversi tersirat yang dapat diterapkan pada argumen b dan c, tetapi ini agak di luar cakupan artikel, jika Anda tertarik, saya sarankan merujuk ke bagian Operator Bersyarat [expr.cond] dari Standar.
int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw void, n ((1 < 2) ? n : v) = 2; // lvalue, , ((1 < 2) ? n : int(123)) = 2; // , .. prvalue
Referensi ke bidang dan metode kelas dan struktur
Untuk ekspresi bentuk am
dan p->m
(di sini kita berbicara tentang operator bawaan ->
), aturan berikut ini berlaku:
- jika
m
adalah elemen enumerasi atau metode kelas non-statis, maka seluruh ekspresi dianggap sebagai nilai awal (meskipun tautan tidak dapat diinisialisasi dengan ekspresi seperti itu); - jika
a
adalah nilai dan m
adalah bidang non-statis dari tipe non-referensi, maka seluruh ekspresi termasuk dalam kategori nilai x ; - kalau tidak, itu adalah nilai .
Untuk pointer ke anggota kelas ( a.*mp
dan p->*mp
), aturannya serupa:
- jika
mp
adalah pointer ke metode kelas, maka seluruh ekspresi dianggap sebagai nilai ; - jika
a
adalah nilai, dan mp
adalah penunjuk ke bidang data, maka seluruh ekspresi merujuk ke nilai x ; - kalau tidak, itu adalah nilai .
Bidang bit
Bidang bit adalah alat yang mudah digunakan untuk pemrograman tingkat rendah, namun implementasinya agak berada di luar struktur umum kategori ekspresi. Misalnya, panggilan ke bidang bit tampaknya merupakan nilai tinggi , karena mungkin ada di sisi kiri operator penugasan. Pada saat yang sama, itu tidak akan berfungsi untuk mengambil alamat bidang bit atau menginisialisasi tautan yang tidak konstan oleh mereka. Anda dapat menginisialisasi referensi konstan ke bidang bit, tetapi salinan sementara objek akan dibuat:
Bidang-bit [class.bit]
Jika penginisialisasi untuk referensi tipe const T & adalah nilai yang merujuk ke bidang-bit, referensi terikat ke sementara yang diinisialisasi untuk menyimpan nilai bidang-bit; referensi tidak terikat ke bidang bit secara langsung.
struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; // auto& rb = bf; //
Alih-alih sebuah kesimpulan
Seperti yang saya sebutkan dalam pendahuluan, uraian di atas tidak mengklaim lengkap, tetapi hanya memberikan gambaran umum tentang kategori ekspresi. Pandangan ini akan memberikan pemahaman yang sedikit lebih baik dari paragraf Standar dan pesan kesalahan kompiler.