Semua chapter sebelumnya menghitung biaya copy dan move; sekarang kita belajar kapan biaya itu bisa mencapai nol total. Copy elision adalah optimasi di mana compiler melewati copy dan move sama sekali — objek dibangun langsung di lokasi tujuannya. Sejak C++17, elision untuk prvalue bahkan bukan optimasi lagi: standar menjaminnya, sehingga return Jejak("x"); dijamin mengonstruksi objek langsung di variabel penerima, di compiler mana pun. Bukti paling kuat yang bisa kita tulis: class yang copy dan move constructornya di-delete tetap bisa dikembalikan by value. Chapter ini membuktikan semuanya dengan jejak konstruksi, lalu menetapkan batasnya — situasi yang tidak dijamin standar.

Elision yang Dijamin: prvalue #

Aturan C++17: ketika sebuah ekspresi prvalue diinisialisasikan, objek sementara tidak pernah dibuat lebih dulu — prvalue tersebut adalah objek tujuan. Akibatnya untuk fungsi seperti Jejak buatPrvalue() { return Jejak("prvalue"); }, tidak ada perpindahan antar-objek yang perlu dihilangkan: objeknya memang lahir di tempat a berada. Perbedaan ini lebih dalam dari optimasi biasa. Optimasi boleh memperhatikan atau tidak; jaminan standar membuat perilakunya bagian dari kontrak bahasa, dan copy/move constructor bahkan tidak perlu ada.

Program: Jejak Konstruksi yang Turun #

cpp
// copy-elision.cpp — C++17 menjamin prvalue dikonstruksi langsung di tujuan
// Kompilasi: g++ -std=c++23 -Wall -Wextra copy-elision.cpp -o copy-elision

#include <print>
#include <string>
#include <utility>

class Jejak {
public:
    explicit Jejak(std::string label) : label_(std::move(label)) {
        std::println("  [ctor ] '{}' dikonstruksi", label_);
    }
    Jejak(const Jejak& lain) : label_(lain.label_) {
        std::println("  [copy ] '{}' disalin", label_);
    }
    Jejak(Jejak&& lain) noexcept : label_(std::move(lain.label_)) {
        std::println("  [move ] '{}' dipindahkan", label_);
    }
    Jejak& operator=(const Jejak&) = default;
    Jejak& operator=(Jejak&&) noexcept = default;
    const std::string& label() const { return label_; }
private:
    std::string label_;
};

// Mengembalikan prvalue: C++17 MENJAMIN objek dikonstruksi langsung di tujuan
Jejak buatPrvalue() {
    return Jejak("prvalue");
}

// Class tanpa copy constructor dan tanpa move constructor sama sekali:
// return by value tetap sah karena elision prvalue dijamin, bukan pilihan
class Murni {
public:
    Murni() { std::println("  [ctor ] Murni (tanpa copy/move constructor)"); }
    Murni(const Murni&) = delete;
    Murni(Murni&&) = delete;
    Murni& operator=(const Murni&) = delete;
    Murni& operator=(Murni&&) = delete;
    void perkenalan() const {
        std::println("  Murni bisa dipakai seperti objek biasa");
    }
};

Murni buatMurni() {
    return Murni{};
}

// Yang dikembalikan adalah PARAMETER: elision tidak dijamin -> move/copy wajib
Jejak dariParameter(Jejak p) {
    return p;                       // p adalah parameter, bukan prvalue
}

// Yang dikembalikan adalah named local: NRVO, opsional (bukan jaminan standar)
Jejak dariNamed() {
    Jejak lokal("named");
    return lokal;
}

int main() {
    std::println("-- prvalue: guaranteed copy elision (C++17)");
    Jejak a = buatPrvalue();
    std::println("  a.label()='{}'", a.label());

    std::println("-- class tanpa copy/move: return by value tetap dikompilasi");
    Murni m = buatMurni();
    m.perkenalan();

    std::println("-- parameter yang dikembalikan: elision tidak dijamin");
    Jejak b = dariParameter(Jejak("parameter"));
    std::println("  b.label()='{}'", b.label());

    std::println("-- named local: NRVO opsional (pembahasan penuh di chapter RVO)");
    Jejak c = dariNamed();
    std::println("  c.label()='{}'", c.label());
    return 0;
}

Output yang Dihasilkan:

text
-- prvalue: guaranteed copy elision (C++17)
  [ctor ] 'prvalue' dikonstruksi
  a.label()='prvalue'
-- class tanpa copy/move: return by value tetap dikompilasi
  [ctor ] Murni (tanpa copy/move constructor)
  Murni bisa dipakai seperti objek biasa
-- parameter yang dikembalikan: elision tidak dijamin
  [ctor ] 'parameter' dikonstruksi
  [move ] 'parameter' dipindahkan
  b.label()='parameter'
-- named local: NRVO opsional (pembahasan penuh di chapter RVO)
  [ctor ] 'named' dikonstruksi
  c.label()='named'

Mari kita bedah:

  1. Baris 62 (Jejak a = buatPrvalue();): hanya satu label [ctor ] untuk seluruh proses — dari return Jejak("prvalue") sampai a, tidak ada copy dan tidak ada move. Sebelum C++17, compiler bebas menambahkan jalur copy/move di sini; sekarang jalur itu dihapus dari bahasa.
  2. Baris 66-67 (class Murni): ini bukti terkuat. Murni men-delete copy dan move constructor (baris 36-37), sehingga tidak ada cara apa pun menyalin objeknya — tetapi Murni m = buatMurni(); tetap dikompilasi dan berjalan, karena elision prvalue dijamin dan constructor yang di-delete tidak pernah dibutuhkan.
  3. Baris 70 (dariParameter(Jejak("parameter"))): argument prvalue dikonstruksi langsung sebagai parameter p (masih tanpa copy/move), tetapi return p; di baris 51 mengembalikan parameter — bukan prvalue — sehingga standar memaksa implicit move: label [move ] muncul. Elision tidak dijamin di sini, dan compiler kita memilih move.
  4. Baris 74 (dariNamed()): lokal adalah named local. NRVO (membangun lokal langsung di tujuan) diperbolehkan tetapi tidak dijamin — compiler GCC 14.2 mengelisinya, sehingga hanya satu [ctor ] tercetak. Compiler lain boleh berbeda; jaminannya ada di implicit move.
  5. Baris 70 vs 62 (jumlah konstruksi): inilah bukti “jumlah konstruksi turun” yang dicari: jalur prvalue menghasilkan tepat satu konstruksi; jalur parameter menghasilkan satu konstruksi plus satu move. Pergeseran satu label [move ] adalah seluruh perbedaan di antara kedua desain fungsi.

Ketika Elision Tidak Dijamin #

Tiga situasi membuat jaminan prvalue tidak berlaku, dan semuanya punya akar yang sama — yang dikembalikan bukan prvalue, melainkan objek yang sudah eksis:

  • Parameter fungsi yang dikembalikan (return p;) — objeknya dibangun oleh pemanggil di lokasi lain; standar mengharuskan implicit move.
  • Named local (return lokal;) — standar mengizinkan NRVO, tetapi tidak mewajibkannya; jaring pengamannya adalah implicit move.
  • Pernyataan return bercabang yang mengembalikan objek lokal berbeda (if (kondisi) return a; return b;) — NRVO mustahil karena dua kandidat objek tidak mungkin berdua menempati satu lokasi tujuan; implicit move yang bekerja.

Ketiga kasus itu tetap murah — implicit move hampir selalu murah — tetapi bedanya dengan elision adalah kepastian. Untuk desain API, konsekuensinya praktis: fungsi yang mengembalikan T(...) (prvalue) menawarkan jaminan konstruksi tunggal, dan std::make_unique beserta saudara-saudaranya didesain memanfaatkan jalur ini.

Kasus named local dan pernyataan bercabang menyinggung strategi compiler bernama NRVO yang baru disinggung sekilas. Chapter berikutnya membahasnya penuh: RVO versus NRVO, batas-batasnya, dan anti-pola return std::move yang menghambatnya.

Rujukan resmi: cppreference — copy elision .