std::move adalah fungsi yang paling sering disalahpahami di C++ modern. Namanya mengesankan sebuah aksi, padahal fungsi ini tidak memindahkan byte, tidak memanggil constructor, dan tidak mengubah objek mana pun. Yang dilakukannya hanya satu: mengubah ekspresi lvalue menjadi xvalue — alias mengikat permintaan “silakan ambil alih objek ini” pada ekspresi tersebut. Pelaku pindahan yang sesungguhnya adalah move constructor atau move assignment yang kemudian dipilih compiler karena argumennya kini rvalue. Chapter ini membuktikan semua klaim itu dengan class ber-jejak, lalu menutup tiga kesalahan umum: memakai objek setelah move, mengira std::move memindahkan tipe fundamental, dan menulis std::move di pernyataan return.
Program: Cast yang Tidak Melakukan Apa pun #
Class Pesan mencetak label pada setiap copy dan move, sehingga kehadiran atau ketiadaan label menjadi bukti:
// std-move.cpp — std::move hanyalah cast ke rvalue
// Kompilasi: g++ -std=c++23 -Wall -Wextra std-move.cpp -o std-move
#include <print>
#include <string>
#include <type_traits>
#include <utility>
class Pesan {
public:
explicit Pesan(std::string teks) : teks_(std::move(teks)) {}
Pesan(const Pesan& lain) : teks_(lain.teks_) {
std::println(" [copy] isi disalin");
}
Pesan(Pesan&& lain) noexcept : teks_(std::move(lain.teks_)) {
lain.teks_ = "(sudah dipindahkan)"; // moved-from state versi kita
std::println(" [move] isi diambil alih");
}
const std::string& teks() const { return teks_; }
private:
std::string teks_;
};
Pesan buatDenganMove() {
Pesan lokal("dibuat lokal");
return std::move(lokal); // anti-pola: mematikan NRVO (akan dijelaskan)
}
Pesan buatLangsung() {
Pesan lokal("dibuat lokal");
return lokal; // NRVO: dibangun langsung di tujuan
}
int main() {
std::println("-- 1) std::move hanyalah cast: tidak ada constructor dipanggil");
Pesan p("pesan asli");
Pesan&& referensi = std::move(p); // cast saja; tak ada jejak copy/move
std::println(" (tidak ada label di atas) referensi.teks()='{}'", referensi.teks());
static_assert(std::is_same_v<decltype(std::move(p)), Pesan&&>);
std::println("-- 2) barulah move terjadi: penerima rvalue memindahkan");
Pesan q = std::move(p); // move constructor terpilih
std::println(" q.teks()='{}'", q.teks());
std::println(" p.teks()='{}' (moved-from: valid, isi tak terentu)", p.teks());
std::println("-- 3) tipe fundamental: std::move turun menjadi copy biasa");
int x = 7;
int y = std::move(x);
std::println(" x={} y={} (keduanya utuh)", x, y);
std::println("-- 4) return std::move mematikan NRVO");
Pesan salah = buatDenganMove();
Pesan benar = buatLangsung();
std::println(" salah.teks()='{}' benar.teks()='{}'",
salah.teks(), benar.teks());
return 0;
}Output yang Dihasilkan:
-- 1) std::move hanyalah cast: tidak ada constructor dipanggil
(tidak ada label di atas) referensi.teks()='pesan asli'
-- 2) barulah move terjadi: penerima rvalue memindahkan
[move] isi diambil alih
q.teks()='pesan asli'
p.teks()='(sudah dipindahkan)' (moved-from: valid, isi tak terentu)
-- 3) tipe fundamental: std::move turun menjadi copy biasa
x=7 y=7 (keduanya utuh)
-- 4) return std::move mematikan NRVO
[move] isi diambil alih
salah.teks()='dibuat lokal' benar.teks()='dibuat lokal'Mari kita bedah:
- Baris 37-39 (cast tanpa konsekuensi):
Pesan&& referensi = std::move(p);hanya mengikat referensi — tidak ada satu pun label copy atau move tercetak, danreferensi.teks()masih membaca"pesan asli".static_assertdi baris 39 menegaskandecltype(std::move(p))adalahPesan&&: std::move hanya memanipulasi tipe ekspresi. - Baris 42-44 (barulah move terjadi):
Pesan q = std::move(p);membuatpmenjadi rvalue, sehingga move constructor terpilih — label[move]akhirnya muncul. Setelah baris ini,p.teks()membaca penanda"(sudah dipindahkan)"yang kita set di baris 16: objek valid, isinya sudah tidak bisa diandalkan. - Baris 47-49 (tipe fundamental):
int y = std::move(x);menghasilkanx=7 y=7. Move constructor tidak ada untukint, sehinggastd::moveturun menjadi copy biasa. Untuk tipe kecil sepertiintdandouble, menulisstd::movetidak berguna dan hanya menambah derau. - Baris 52-53 (NRVO dimatikan):
buatDenganMove()mencetak satu label[move](karenareturn std::move(lokal)memaksa pindahan), sedangkanbuatLangsung()tidak mencetak apa pun — compiler membangun objeknya langsung di tujuan.
Kesalahan Umum: Memakai Objek Setelah Move #
Setelah std::move dan penerima rvalue menjalankan tugasnya, objek sumber berada dalam keadaan moved-from: valid, tetapi isinya tak terentu. Pada class Pesan, kita sendiri yang menentukan penandanya (baris 16), sehingga output-nya deterministik. Pada tipe pustaka, keadaan itu ditentukan implementasi — std::string hasil pindahan biasanya kosong, tetapi standar tidak menjamin nilai apa pun. Kontrak yang bisa kita andalkan hanya dua: destructor akan berjalan dengan benar, dan objek boleh diberi nilai baru lewat assignment.
Kebiasaan yang aman: perlukan variabel yang baru saja di-move sebagai “sudah diserahkan” — jangan lagi dibaca, kecuali kita menetapkan ulang nilainya lebih dulu.
Jangan std::move di Pernyataan return #
Bagian 4 di atas menunjukkan aturan yang sering keliru: return std::move(lokal); mematikan optimasi. Untuk mengembalikan variabel lokal, compiler sudah boleh membangun objeknya langsung di tujuan (NRVO — nanti dibahas penuh di chapter Return Value Optimization); menambahkan std::move memaksa jalur pindahan dan menggugurkan kesempatan itu. GCC 14 bahkan memperingatkan pola ini lewat flag -Wpessimizing-move yang aktif bersama -Wall:
std-move/std-move.cpp: In function ‘Pesan buatDenganMove()’:
std-move/std-move.cpp:26:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
26 | return std::move(lokal); // anti-pola: mematikan NRVO (akan dijelaskan)
| ~~~~~~~~~^~~~~~~
std-move/std-move.cpp:26:21: note: remove ‘std::move’ callPeringatan itu kita biarkan muncul di contoh ini karena justru itulah pelajarannya: aturan yang benar adalah menulis return lokal; dan membiarkan compiler memilih strategi terbaik — elision bila mungkin, implicit move bila tidak. Note “remove ‘std::move’ call” pada pesan di atas adalah saran yang tepat, bukan sekadar gaya penulisan.
Kekuatan std::move ada pada sisi pemanggil yang memang sudah selesai dengan objeknya. Tetapi bagaimana bila yang menulis kode adalah fungsi perantara generik — wrapper yang tidak tahu apakah pemanggilnya mengirim lvalue atau rvalue? Chapter berikutnya menjawabnya dengan perfect forwarding.
Rujukan resmi: cppreference — std::move .