Lambda pada chapter sebelumnya memindahkan string masuk dengan std::move dan programnya justru makin cepat — sekarang kita bongkar mengapa. Konsepnya sederhana tapi dampaknya besar: copy menduplikasi isi objek (buffer baru dialokasi, data disalin byte demi byte), sedangkan move mengambil alih buffer milik sumber — pointer ditukar, sumber dibiarkan kosong, dan biayanya konstan tidak peduli datanya 10 byte atau 10 juta byte. Untuk tipe pustaka yang punya buffer di heap (std::string, std::vector, std::map), perbedaan ini menentukan performa program sungguhan. Chapter ini membahas move semantics dari sisi pengguna: apa yang terjadi, kapan compiler melakukannya otomatis, dan bagaimana memintanya secara eksplisit dengan std::move. Detail teori penuh — rvalue reference, value category, move assignment, dan rule of five — ditanggung khusus oleh Section Move Semantics dan Value Categories; di sini kita fokus ke kebiasaan pemakaian yang benar.
Program: Mengamati Copy, Move, dan NRVO #
Agar copy dan move bisa teramati, class Dokumen di bawah mencetak label setiap kali constructor-nya dipanggil:
// move-demo.cpp — move semantics: copy vs move, std::move, dan move otomatis
// Kompilasi: g++ -std=c++23 -Wall -Wextra move-demo.cpp -o move-demo
#include <print>
#include <string>
#include <utility>
#include <vector>
struct Dokumen {
std::string isi;
Dokumen(std::string t) : isi(std::move(t)) {}
Dokumen(const Dokumen& lain) : isi(lain.isi) { std::print(" [copy]\n"); }
Dokumen(Dokumen&& lain) noexcept : isi(std::move(lain.isi)) { std::print(" [move]\n"); }
};
Dokumen buat() {
Dokumen d("laporan akhir");
return d; // NRVO: objek dibangun langsung di tujuan
}
Dokumen pilih(bool pakaiA) {
Dokumen a("versi-A"), b("versi-B");
if (pakaiA) return a; // NRVO tak mungkin: move implisit
return b;
}
int main() {
std::print("-- return by value: NRVO\n");
Dokumen x = buat();
std::print("x.isi = {}\n", x.isi);
std::print("-- return di dalam if: move implisit\n");
Dokumen y = pilih(true);
std::print("y.isi = {}\n", y.isi);
std::print("-- copy dari lvalue\n");
Dokumen s1 = x; // lvalue -> copy
std::print("-- std::move: minta dipindahkan\n");
Dokumen s2 = std::move(x); // rvalue -> move
std::print("s2.isi = {}\n", s2.isi);
std::print("-- push_back dengan dan tanpa std::move\n");
std::vector<Dokumen> v;
v.reserve(3);
v.push_back(Dokumen("sementara")); // temporary -> move
v.push_back(s1); // lvalue -> copy
v.push_back(std::move(s1)); // std::move -> move
std::print("v[0]={} v[1]={} v[2]={}\n", v[0].isi, v[1].isi, v[2].isi);
return 0;
}Output yang Dihasilkan:
-- return by value: NRVO
x.isi = laporan akhir
-- return di dalam if: move implisit
[move]
y.isi = versi-A
-- copy dari lvalue
[copy]
-- std::move: minta dipindahkan
[move]
s2.isi = laporan akhir
-- push_back dengan dan tanpa std::move
[move]
[copy]
[move]
v[0]=sementara v[1]=laporan akhir v[2]=laporan akhirMari kita bedah:
- Baris 13-14 (dua constructor): copy constructor menyalin
isimiliklain— alokasi buffer baru; move constructor (tipe parameternyaDokumen&&) memanggilstd::move(lain.isi)sehingga buffer pindah alih-alih disalin. Keduanyanoexcept— alasan pentingnya dibahas di bawah. - Baris 29-31 (NRVO):
buat()mengembalikanDokumenby value, dan tidak ada label yang tercetak — compiler membangun objek langsung di tempatxberada (Return Value Optimization / NRVO). Tidak ada copy, tidak ada move: inilah cara termurah mengembalikan objek, dan alasannya return by value justru dianjurkan. - Baris 33-35 (move implisit): di
pilih(), NRVO tidak mungkin karena ada dua objek kandidat, tetapi compiler otomatis memperlakukanreturn a;sebagai rvalue — label[move]tercetak tanpa kita menulisstd::move. Aturan standar: return sebuah variabel lokal otomatis menjadi move bila NRVO gagal. - Baris 38 vs 41 (lvalue vs rvalue):
Dokumen s1 = x;menyalin karenaxadalah lvalue bernama;Dokumen s2 = std::move(x);memicu[move]karenastd::movemengubahxmenjadi ekspresi rvalue. Setelah baris itu,xjangan dipakai lagi isinya — buffer-nya sudah menjadi miliks2. - Baris 45-50 (pola di container):
push_backdengan temporary memicu move; dengan lvalue memicu copy; denganstd::move(s1)memicu move. Pola ketiga inilah cara standar mengosongkan variabel sementara masuk kestd::vectortanpa salinan.v.reserve(3)di baris 46 menjaga jumlah label tetap bersih — tanpa itu, realokasistd::vectorakan memicu move tambahan untuk setiap elemen lama yang direlokasi.
std::move Tidak Memindahkan Apa pun #
Nama std::move menyesatkan sekaligus menginspirasi pemahaman yang benar: fungsi ini tidak memindahkan byte, tidak memanggil constructor apa pun, dan tidak mengubah objek sumber. Ia hanya cast — mengubah ekspresi lvalue menjadi rvalue reference, alias menempelkan permintaan “silakan pindahkan objek ini”. Pelaku pindahan yang sesungguhnya adalah move constructor atau move assignment yang dipilih compiler ketika argumennya rvalue. Karena itu std::move pada tipe tanpa move constructor (mis. tipe yang hanya punya copy) tidak error — ia turun menjadi copy, diam-diam.
Konsekuensi pentingnya adalah keadaan objek sumber setelah dipindahkan (moved-from state). Standar menjamin objek itu tetap valid — destructornya jalan, operator= masih boleh dipanggil — tetapi tidak menjamin nilai isinya. std::string hasil pindahan biasanya kosong, tetapi “biasanya” bukan jaminan; bila kita menulis fungsi yang menerima objek pindahan, kita tidak boleh mengandalkan isinya kecuali kita sendiri yang mengatur ulang. Inilah kontrak move: sumber adalah objek valid dengan nilai tak tentu, tujuan memiliki seluruh isinya.
Kapan Move Terjadi Otomatis #
Ada empat situasi yang memicu move tanpa kita menulis apa pun. Pertama, return sebuah temporary: return Dokumen("baru"); — objek dibangun langsung di tujuan (elisi). Kedua, return variabel lokal: compiler mencoba NRVO dulu; bila tidak mungkin (objek kandidat lebih dari satu, atau yang dikembalikan adalah parameter), standar memerintahkan implicit move. Ketiga, melempar dan menangkap exception by value. Keempat, operasi container standar: std::vector::push_back pada temporary, realokasi memindahkan elemen lama ke buffer baru, dan std::ranges::sort menukar elemen lewat move.
Situasi keempat menjelaskan kenapa move constructor di baris 14 ditandai noexcept. Saat std::vector tumbuh dan harus memindahkan elemennya, ia memilih antara move (aman, cepat) dan copy (aman terhadap exception tetapi mahal) — dan ia hanya berani move bila move constructor dijamin tidak melempar. Tanpa noexcept, class yang menampung std::string besar akan kalah performa di container tanpa kita sadari. Praktik yang benar: tandai move constructor dan move assignment noexcept kecuali ada alasan kuat yang didokumentasikan.
Aturan Praktis Pemakaian #
Dari semua mekanisme di atas, kebiasaan sehari-hari bisa diringkas pendek:
- Kembalikan objek by value — NRVO dan implicit move membuatnya hampir selalu optimal; jangan kembalikan pointer/reference hanya demi menghindari “salinan” yang sebenarnya tidak terjadi.
std::movehanya saat selesai dengan sumbernya — untuk variabel lokal di akhir hidupnya, atau saat memasukkan objek berat ke container.- Jangan
std::movekembalian fungsi —return std::move(x);justru mematikan NRVO; returnxlangsung. - Tandai move constructor/assignment
noexceptagar container berani memindahkan elemen kita. - Tipe kecil (
int,double, pointer) tidak perlustd::move— menyalinnya lebih murah daripada memindahkan.
Satu sisa pertanyaan yang belum kita jawab: bagaimana sebuah fungsi generik bisa menerima argumen lvalue maupun rvalue dan meneruskannya tanpa menggugurkan kualitas pindahannya? std::move saja tidak cukup, karena ia selalu menghasilkan rvalue — padahal pemanggil mungkin ingin lvalue tetap lvalue. Jawabannya adalah std::forward dan forwarding reference, topik penuh chapter berikutnya.
Rujukan resmi: cppreference — std::move , cppreference — move constructor .