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 return mengembalikan ekspresi prvalue seperti return Laporan("RVO");. Karena prvalue adalah objek tujuan (aturan C++17), tidak ada objek perantara yang bisa dihilangkan — jaminannya mutlak.
  • NRVO terjadi ketika return mengembalikan named local seperti return lokal;. Compiler boleh membangun lokal langsung 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 #

cpp
// 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:

text
-- 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:

  1. Baris 55 (Laporan x = rvo();): jalur prvalue — satu [ctor], nol copy, nol move. Inilah bentuk yang dijamin standar.
  2. 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.
  3. Baris 61 (Laporan z = pilih(1);): fungsi pilih() memiliki dua kandidat objek (a dan b); tidak ada satu lokasi memori yang bisa ditempati dua objek sekaligus, sehingga NRVO mustahil. Standar memerintahkan implicit move — label [move] kandidat-A tercetak tanpa kita menulis apa pun.
  4. 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 dibanding nrvo().
  5. 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 kasus pilih() 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:

text
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’ call

Peringatan 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 .