Chapter Copy Elision menetapkan jaminannya; chapter ini membahas dua nama strategi yang sering disebut bersamaan: RVO (Return Value Optimization) untuk kasus mengembalikan prvalue — sejak C++17 dijamin oleh standar — dan NRVO (Named RVO) untuk kasus mengembalikan variabel lokal bernama — diperbolehkan, tetapi tidak dijamin. Perbedaan status keduanya punya dampak praktis pada cara kita menulis fungsi dan membaca kode orang lain. Chapter ini mengukur keduanya dengan class ber-jejak pada compiler yang sama, memperlihatkan batas NRVO yang mustahil dilanggar, dan menghitung biaya anti-pola return std::move(lokal);.
RVO: Dijamin; NRVO: Opsional #
Keduanya mencapai hasil yang sama — objek kembaliannya dibangun langsung di variabel penerima, tanpa copy dan tanpa move — tetapi dari jalur berbeda:
- RVO terjadi ketika
returnmengembalikan ekspresi prvalue sepertireturn Laporan("RVO");. Karena prvalue adalah objek tujuan (aturan C++17), tidak ada objek perantara yang bisa dihilangkan — jaminannya mutlak. - NRVO terjadi ketika
returnmengembalikan named local sepertireturn lokal;. Compiler boleh membangunlokallangsung di slot kembalian, tetapi boleh juga tidak; bila memilih tidak, implicit move yang mengambil alih. Jaminan hasilnya tetap murah, hanya bukan “nol operasi”.
Praktiknya pada compiler yang kita pakai: GCC 14.2 menjalankan NRVO bahkan pada -O0 untuk jalur return tunggal, sehingga kedua strategi menghasilkan jejak yang sama di program di bawah. Yang berbeda adalah jaminannya — kode kita sebaiknya tidak bergantung pada perilaku NRVO yang tidak terjamin.
Program: Empat Jalur Return dalam Satu Ukuran #
// return-value-optimization.cpp — RVO vs NRVO: kapan dijamin, kapan gagal
// Kompilasi: g++ -std=c++23 -Wall -Wextra return-value-optimization.cpp -o return-value-optimization
#include <print>
#include <string>
#include <utility>
class Laporan {
public:
explicit Laporan(std::string nama) : nama_(std::move(nama)) {
std::println(" [ctor] {}", nama_);
}
Laporan(const Laporan& lain) : nama_(lain.nama_) {
std::println(" [copy] {}", nama_);
}
Laporan(Laporan&& lain) noexcept : nama_(std::move(lain.nama_)) {
std::println(" [move] {}", nama_);
}
Laporan& operator=(const Laporan&) = default;
Laporan& operator=(Laporan&&) noexcept = default;
const std::string& nama() const { return nama_; }
private:
std::string nama_;
};
// RVO: mengembalikan prvalue — DIJAMIN standar sejak C++17
Laporan rvo() {
return Laporan("RVO-prvalue");
}
// NRVO: mengembalikan named local — OPSIONAL; GCC 14.2 mengelisinya
Laporan nrvo() {
Laporan lokal("NRVO-named");
return lokal;
}
// Dua kandidat objek: NRVO mustahil -> implicit move yang bekerja
Laporan pilih(int kode) {
Laporan a("kandidat-A");
Laporan b("kandidat-B");
if (kode == 1) {
return a;
}
return b;
}
// Anti-pola: std::move pada named local mematikan NRVO
Laporan nrvoRusak() {
Laporan lokal("NRVO-dimatikan");
return std::move(lokal);
}
int main() {
std::println("-- RVO (prvalue): dijamin standar");
Laporan x = rvo();
std::println("-- NRVO (named local): opsional, di GCC ini tetap dielisi");
Laporan y = nrvo();
std::println("-- dua kandidat objek: NRVO mustahil, move implisit");
Laporan z = pilih(1);
std::println("-- anti-pola: return std::move(lokal)");
Laporan w = nrvoRusak();
std::println("x='{}' y='{}' z='{}' w='{}'",
x.nama(), y.nama(), z.nama(), w.nama());
return 0;
}Output yang Dihasilkan:
-- RVO (prvalue): dijamin standar
[ctor] RVO-prvalue
-- NRVO (named local): opsional, di GCC ini tetap dielisi
[ctor] NRVO-named
-- dua kandidat objek: NRVO mustahil, move implisit
[ctor] kandidat-A
[ctor] kandidat-B
[move] kandidat-A
-- anti-pola: return std::move(lokal)
[ctor] NRVO-dimatikan
[move] NRVO-dimatikan
x='RVO-prvalue' y='NRVO-named' z='kandidat-A' w='NRVO-dimatikan'Mari kita bedah:
- Baris 55 (
Laporan x = rvo();): jalur prvalue — satu[ctor], nol copy, nol move. Inilah bentuk yang dijamin standar. - Baris 58 (
Laporan y = nrvo();): jalur named local — GCC mengelisinya, jadi jejaknya sama persis dengan RVO. Pada compiler lain, baris ini berpotensi mencetak satu[move]tambahan; standar mengizinkan keduanya. - Baris 61 (
Laporan z = pilih(1);): fungsipilih()memiliki dua kandidat objek (adanb); tidak ada satu lokasi memori yang bisa ditempati dua objek sekaligus, sehingga NRVO mustahil. Standar memerintahkan implicit move — label[move] kandidat-Atercetak tanpa kita menulis apa pun. - Baris 64 (
Laporan w = nrvoRusak();):return std::move(lokal);mengubah named local menjadi xvalue secara paksa. Compiler kehilangan kesempatan NRVO dan menurut dengan jalur move: satu[ctor]plus satu[move]— satu operasi lebih banyak dibandingnrvo(). - Baris 66-67 (verifikasi isi): keempat objek tetap membawa isi yang benar; optimasi ini tidak mengubah makna program, hanya jumlah operasi pembangunannya.
Batas NRVO yang Tidak Bisa Didesak #
NRVO hanya mungkin bila semua jalur return mengembalikan objek yang sama. Tiga kondisi yang menggugurkannya:
- Cabang dengan objek berbeda:
if (kode == 1) return a; return b;— persis kasuspilih()di atas. - Yang dikembalikan adalah parameter: parameter dibangun oleh pemanggil di lokasinya sendiri; standar menetapkan implicit move, bukan elision.
- Yang dikembalikan adalah anggota objek (mis.
return this->nama_;): objek itu sudah eksis sebelum fungsi dipanggil; yang mungkin hanya move atau copy.
Bila desain fungsi memaksa jalur-jalur ini, jangan dipaksakan berubah demi elision — implicit move sudah memberikan biaya yang wajar. Tapi bila kita memilih desain, mengembalikan prvalue (return Laporan{...};) adalah satu-satunya bentuk dengan jaminan konstruksi tunggal.
Anti-pola: return std::move(lokal) #
Baris 50 di atas adalah kesalahan yang beredar luas di kode lama: asumsi “menambahkan std::move membuat kembalian lebih cepat”. Kenyataannya sebaliknya — std::move mematikan NRVO yang sudah gratis, lalu menawarkan jalur move yang lebih lambat dari nol operasi. Pada chapter std::move kita sudah melihat GCC memperingatkan pola ini lewat -Wpessimizing-move; untuk file ini, peringatan yang muncul saat kompilasi adalah:
return-value-optimization/return-value-optimization.cpp: In function ‘Laporan nrvoRusak()’:
return-value-optimization/return-value-optimization.cpp:50:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
50 | return std::move(lokal);
| ~~~~~~~~~^~~~~~~
return-value-optimization/return-value-optimization.cpp:50:21: note: remove ‘std::move’ callPeringatan itu sengaja dibiarkan menyala di contoh ini — ia adalah alarm yang tepat untuk pola yang salah. Aturan yang selamat dari semua pembahasan ini ringkas: return lokal; untuk variabel lokal, return T{...}; untuk jaminan maksimum, dan tidak pernah return std::move(...) pada variabel lokal.
Sampai di sini, semua mesin move — reference, constructor, assignment, forwarding, elision — sudah kita kendalikan satu per satu. Pertanyaan yang tersisa bersifat desain: kapan kita menulis kelima fungsi khusus itu sendiri, kapan cukup mendelegasikan ke member, dan kapan justru melarangnya? Aturan Rule of Three, Five, dan Zero menjawabnya — sekaligus menutup section ini.
Rujukan resmi: cppreference — copy elision , cppreference — return statement .