Move semantics (sejak C++11) memungkinkan transfer resource antar objek tanpa menyalin data. Alih-alih menyalin 1 juta elemen int dari satu vector ke yang lain (operasi O(n) dengan alokasi heap besar), kita cukup menukar pointer internal — operasi O(1). Untuk DSA, ini berarti mengembalikan container besar dari fungsi, memindahkan node pohon, atau menyimpan string ke vector bisa dilakukan tanpa copy mahal. Pada Chapter ini kita akan membedah rvalue reference, std::move, move constructor/assignment, dan benchmark copy vs move pada buffer berukuran 4 MB.
Komponen Utama #
| Komponen | Sintaks | Fungsi |
|---|---|---|
| Rvalue reference | T&& | Referensi yang hanya mengikat ke rvalue (mis. sementara atau std::move-ed) |
std::move(x) | Cast x ke T&& | Menandai x siap dipindahkan |
| Move constructor | T(T&&) | Membangun objek dengan mencuri resource dari sumber |
| Move assignment | T& operator=(T&&) | Mencuri resource, melepaskan resource lama |
Mekanisme: lvalue, rvalue, dan Resource Stealing #
- lvalue: objek dengan alamat (variabel,
*ptr,arr[i]). Bisa di-assign. - rvalue: objek sementara tanpa nama (literal, hasil fungsi,
std::move(x)). - Referensi
T&mengikat lvalue;T&&mengikat rvalue. OverloadT&vsT&&memilih konstruktor/assignment mana yang dipanggil.
Saat kita menulis Buffer c = std::move(a), compiler memilih move constructor. Move constructor menukar pointer internal: c.data_ = a.data_ (steal), lalu a.data_ = nullptr (mencegah double-free). Resource 1 juta elemen dipindahkan tanpa menyalin satu elemen pun.
std::move sendiri tidak memindahkan apa pun — ia hanya cast x ke rvalue reference. Pemindahan resource terjadi di move constructor/assignment yang dipilih karena cast tersebut.
Kode: Buffer dengan Copy dan Move #
#include <iostream>
#include <vector>
#include <string>
#include <utility>
#include <chrono>
class Buffer {
size_t size_;
int* data_;
public:
// Constructor
Buffer(size_t n) : size_(n), data_(new int[n]) {
std::cout << " ctor size=" << size_ << '\n';
}
// Destructor
~Buffer() {
delete[] data_;
std::cout << " dtor size=" << size_ << '\n';
}
// Copy constructor (deep copy — mahal)
Buffer(const Buffer& o) : size_(o.size_), data_(new int[o.size_]) {
for (size_t i = 0; i < size_; ++i) data_[i] = o.data_[i];
std::cout << " copy ctor size=" << size_ << '\n';
}
// Copy assignment
Buffer& operator=(const Buffer& o) {
if (this != &o) {
delete[] data_;
size_ = o.size_;
data_ = new int[size_];
for (size_t i = 0; i < size_; ++i) data_[i] = o.data_[i];
}
std::cout << " copy assign size=" << size_ << '\n';
return *this;
}
// Move constructor (steal — murah)
Buffer(Buffer&& o) noexcept : size_(o.size_), data_(o.data_) {
o.size_ = 0;
o.data_ = nullptr;
std::cout << " move ctor size=" << size_ << '\n';
}
// Move assignment
Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) {
delete[] data_;
size_ = o.size_;
data_ = o.data_;
o.size_ = 0;
o.data_ = nullptr;
}
std::cout << " move assign size=" << size_ << '\n';
return *this;
}
size_t size() const { return size_; }
};
Buffer make_buffer(size_t n) {
Buffer b(n);
return b; // RVO/move
}
int main() {
std::cout << "--- 1) copy ctor ---\n";
Buffer a(100);
Buffer b = a; // copy ctor
std::cout << "\n--- 2) move ctor ---\n";
Buffer c = std::move(a); // move ctor — a sekarang kosong
std::cout << "a.size()=" << a.size() << ", c.size()=" << c.size() << '\n';
std::cout << "\n--- 3) return value (NRVO/move) ---\n";
Buffer d = make_buffer(50);
std::cout << "d.size()=" << d.size() << '\n';
std::cout << "\n--- 4) push_back ke vector ---\n";
std::vector<Buffer> v;
v.reserve(3);
Buffer tmp(10);
v.push_back(tmp); // copy (lvalue)
v.push_back(std::move(tmp)); // move (rvalue)
std::cout << "\n--- 5) benchmark copy vs move ---\n";
const size_t N = 1'000'000;
Buffer big(N);
auto t1 = std::chrono::high_resolution_clock::now();
Buffer copy_buf = big; // deep copy
auto t2 = std::chrono::high_resolution_clock::now();
auto t3 = std::chrono::high_resolution_clock::now();
Buffer move_buf = std::move(big); // move — hanya pointer swap
auto t4 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::micro> dc = t2 - t1;
std::chrono::duration<double, std::micro> dm = t4 - t3;
std::cout << "copy 1M int: " << dc.count() << " us\n";
std::cout << "move 1M int: " << dm.count() << " us\n";
return 0;
}Output yang Dihasilkan (running lokal):
--- 1) copy ctor ---
ctor size=100
copy ctor size=100
--- 2) move ctor ---
move ctor size=100
a.size()=0, c.size()=100
--- 3) return value (NRVO/move) ---
ctor size=50
d.size()=50
--- 4) push_back ke vector ---
ctor size=10
copy ctor size=10
move ctor size=10
--- 5) benchmark copy vs move ---
ctor size=1000000
copy ctor size=1000000
move ctor size=1000000
copy 1M int: 1705.15 us
move 1M int: 0.215 us
dtor size=1000000
dtor size=1000000
dtor size=0
dtor size=0
dtor size=10
dtor size=10
dtor size=50
dtor size=100
dtor size=100
dtor size=0Bedah Kode Baris-per-Baris #
Buffer b = a(copy ctor): parameteraadalah lvalue → copy constructor dipilih. Bufferbmengalokasikan array baru dan menyalin 100 elemen.atetap utuh.Buffer c = std::move(a)(move ctor):std::move(a)meng-castakeBuffer&&→ move constructor dipilih.c.data_menerima pointera.data_;a.data_ = nullptr,a.size_ = 0. Tidak ada alokasi/memcpy. Setelah move,aberada dalam valid but unspecified state — aman untuk destruct atau re-assign.make_buffer(50): fungsi mengembalikanBufferby-value. Karenabadalah local lvalue, kita sebut NRVO mungkin, atau move constructor dipilih bila RVO tidak terjadi. Output menunjukkan satuctor(di dalam fungsi) — RVO menghilangkan move. Ini optimalisasi compiler, bukan perilaku wajib.v.reserve(3): memesan kapasitas 3 sehingga push_back berikutnya tidak memicu realokasi (yang akan mem-move seluruh elemen).v.push_back(tmp):tmpadalah lvalue → copy constructor. Resourcetmptetap utuh.v.push_back(std::move(tmp)):tmpdi-cast ke rvalue → move constructor. Resource dipindahkan;tmpsekarang kosong tetapi masih hidup sampai akhir scope.- Benchmark
Buffer copy_buf = big: copy ctor mengalokasikan array 1Mint(~4 MB) lalu memcpy. Waktu~1700 µs(1.7 ms) — sekitar 2.3 GB/s (konstan memcpy modern). - Benchmark
Buffer move_buf = std::move(big): move ctor hanya menyalin dua pointer (size_,data_) dan men-nolkan sumbernya. Waktu~0.2 µs— 8000× lebih cepat dari copy.
Move di STL Container #
Container STL sudah mendukung move sejak C++11:
std::vector::push_back(T&&)— move constructor elemen dipanggil bila argumen adalah rvalue.std::vector::insertoverload denganT&&.std::string,std::vector,std::map, dst. punya move constructornoexcept.
Move membuat operasi seperti v2 = std::move(v1) sangat murah — hanya swap pointer internal. Untuk container berukuran besar, ini mengubah O(n) copy menjadi O(1) move.
noexcept Move Constructor #
Move constructor sebaiknya selalu noexcept:
Buffer(Buffer&& o) noexcept { ... }std::vector saat realokasi memeriksa apakah move ctor noexcept — bila ya, ia pakai move; bila tidak, ia pakai copy untuk menjaga strong exception guarantee. Tanpa noexcept, Anda kehilangan benefit move pada vector::reserve/push_back saat realokasi.
Pola Umum di DSA #
Sink Function #
void push_value(std::vector<int>& v, int x) {
v.push_back(x); // copy OK untuk int
}
void push_value(std::vector<std::string>& v, std::string s) {
v.push_back(std::move(s)); // move string
}Return Large Object #
std::vector<int> compute(int n) {
std::vector<int> result;
result.reserve(n);
// ... isi result ...
return result; // RVO atau move
}Move-Only Type #
std::unique_ptr tidak bisa di-copy; hanya bisa di-move. Ini memastikan kepemilikan eksklusif:
auto p = std::make_unique<int>(42);
auto q = std::move(p); // p sekarang nullptr
Ringkasan #
- Rvalue reference
T&&+std::move(x)memilih move constructor/assignment. - Move constructor “mencuri” resource (pointer swap), mengubah
O(n)copy menjadiO(1)move. std::movehanya cast; pemindahan resource terjadi di move ctor/assignment.- Setelah
std::move(x),xvalid tetapi tidak terdefinisi nilainya — hanya destruct atau re-assign yang aman. - Selalu tandai move ctor/assignment
noexceptagar STL memakai move saat realokasi. - Pola sink:
void set(T x) { member_ = std::move(x); }— satu overload melayani copy dan move.