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:

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

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

  1. Baris 37-39 (cast tanpa konsekuensi): Pesan&& referensi = std::move(p); hanya mengikat referensi — tidak ada satu pun label copy atau move tercetak, dan referensi.teks() masih membaca "pesan asli". static_assert di baris 39 menegaskan decltype(std::move(p)) adalah Pesan&&: std::move hanya memanipulasi tipe ekspresi.
  2. Baris 42-44 (barulah move terjadi): Pesan q = std::move(p); membuat p menjadi 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.
  3. Baris 47-49 (tipe fundamental): int y = std::move(x); menghasilkan x=7 y=7. Move constructor tidak ada untuk int, sehingga std::move turun menjadi copy biasa. Untuk tipe kecil seperti int dan double, menulis std::move tidak berguna dan hanya menambah derau.
  4. Baris 52-53 (NRVO dimatikan): buatDenganMove() mencetak satu label [move] (karena return std::move(lokal) memaksa pindahan), sedangkan buatLangsung() 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:

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

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