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 #
// 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:
-- 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:
- Baris 62 (
Jejak a = buatPrvalue();): hanya satu label[ctor ]untuk seluruh proses — darireturn Jejak("prvalue")sampaia, tidak ada copy dan tidak ada move. Sebelum C++17, compiler bebas menambahkan jalur copy/move di sini; sekarang jalur itu dihapus dari bahasa. - Baris 66-67 (class
Murni): ini bukti terkuat.Murnimen-delete copy dan move constructor (baris 36-37), sehingga tidak ada cara apa pun menyalin objeknya — tetapiMurni m = buatMurni();tetap dikompilasi dan berjalan, karena elision prvalue dijamin dan constructor yang di-delete tidak pernah dibutuhkan. - Baris 70 (
dariParameter(Jejak("parameter"))): argument prvalue dikonstruksi langsung sebagai parameterp(masih tanpa copy/move), tetapireturn 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. - Baris 74 (
dariNamed()):lokaladalah named local. NRVO (membangunlokallangsung 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. - 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 .