Move Semantics ve Rvalue References
C++ programlarında en maliyetli işlemlerden biri gereksiz kopyalamadır. Büyük bir string, bir milyon elemanlı vector veya ağır bir nesneyi kopyalamak hem zaman hem bellek harcar. Move semantics, bu sorunu çözen devrim niteliğinde bir özelliktir.
Bir taşınma analojisi düşün: eski evden yeni eve çıkıyorsun. Tüm eşyalarını birebir kopyalayıp yeni eve koymak yerine (copy), kamyona yükleyip taşırsın (move). Eski evde hiçbir şey kalmaz ama yeni eve her şey gelir — ve bu çok daha hızlı.
lvalue vs rvalue Nedir?
Move semantics'i anlamak için önce değer kategorilerini (value categories) bilmelisin.
lvalue (left value): Bellekte kalıcı bir adresi olan, ismi olan ifade. Bir değişken, bir dizi elemanı, bir referans — bunlar lvalue'dur. "Sol tarafta durabilir" diye hatırla.
rvalue (right value): Geçici olan, ismi olmayan, satır sonunda yok olacak ifade. Bir literal, bir fonksiyonun döndürdüğü geçici nesne, bir aritmetik ifadenin sonucu — bunlar rvalue'dur.
#include <string>
#include <iostream>
int main() {
int x = 42; // x → lvalue, 42 → rvalue
int y = x + 10; // y → lvalue, (x + 10) → rvalue
std::string name = "Ali"; // name → lvalue, "Ali" → rvalue
std::string greeting = name + "!"; // greeting → lvalue, (name + "!") → rvalue
// lvalue'nun adresi alınabilir
int* ptr = &x; // OK — x'in adresi var
// rvalue'nun adresi alınamaz
// int* ptr2 = &42; // HATA! 42'nin adresi yok
// int* ptr3 = &(x + 10); // HATA! Geçici ifadenin adresi yok
return 0;
}Pratik kural: Eğer bir ifadenin adresini & ile alabiliyorsan lvalue'dur, alamıyorsan rvalue'dur.
Rvalue Reference (&&)
C++11, rvalue reference kavramını getirdi. && ile tanımlanan bu referans türü, sadece rvalue'lara bağlanır.
#include <string>
#include <iostream>
int main() {
int x = 42;
// Normal (lvalue) referans — sadece lvalue'ya bağlanır
int& lref = x; // OK
// int& lref2 = 42; // HATA! rvalue'ya bağlanamaz
// Rvalue referans — sadece rvalue'ya bağlanır
int&& rref = 42; // OK
// int&& rref2 = x; // HATA! lvalue'ya bağlanamaz
// const lvalue referans — her ikisine de bağlanır
const int& cref = x; // OK — lvalue
const int& cref2 = 42; // OK — rvalue (istisna!)
std::cout << "rref: " << rref << "\n";
return 0;
}💡 `const T&` hem lvalue hem rvalue kabul eder. Bu yüzden C++11 öncesinde fonksiyon parametreleri genellikle
const T&olarak yazılırdı. Move semantics ile artıkT&&overload'u ekleyerek rvalue'lar için özel (daha hızlı) işlem yapabiliyoruz.
Move Constructor ve Move Assignment
Copy constructor var olan nesneyi kopyalar. Move constructor ise kaynakları taşır — kopyalamadan, sadece pointer'ları el değiştirerek.
#include <iostream>
#include <cstring>
#include <utility>
class MyString {
char* data;
size_t length;
public:
// Constructor
MyString(const char* str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
std::cout << "Constructor: " << data << "\n";
}
// Copy constructor — derin kopya
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
std::cout << "Copy: " << data << "\n";
}
// Move constructor — kaynakları çal
MyString(MyString&& other) noexcept {
data = other.data; // Pointer'ı al
length = other.length;
other.data = nullptr; // Eski nesneyi "boşalt"
other.length = 0;
std::cout << "Move: " << data << "\n";
}
// Destructor
~MyString() {
delete[] data;
}
void print() const {
if (data) std::cout << data << "\n";
else std::cout << "(bos)\n";
}
};
int main() {
MyString a("Merhaba"); // Constructor
MyString b = a; // Copy constructor — a hâlâ geçerli
MyString c = std::move(a); // Move constructor — a artık boş
std::cout << "a: "; a.print(); // (bos)
std::cout << "b: "; b.print(); // Merhaba
std::cout << "c: "; c.print(); // Merhaba
return 0;
}Move Assignment Operator
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : size(n), data(new int[n]{}) {
std::cout << "Buffer(" << n << ") olusturuldu\n";
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::memcpy(data, other.data, size * sizeof(int));
std::cout << "Copy assignment\n";
}
return *this;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // Mevcut kaynağı serbest bırak
data = other.data; // Diğerinin kaynağını al
size = other.size;
other.data = nullptr; // Diğerini boşalt
other.size = 0;
std::cout << "Move assignment\n";
}
return *this;
}
~Buffer() { delete[] data; }
size_t getSize() const { return size; }
};
int main() {
Buffer a(1000);
Buffer b(500);
b = a; // Copy assignment — a hâlâ geçerli
b = Buffer(2000); // Move assignment — geçici nesne taşınır
std::cout << "b size: " << b.getSize() << "\n";
return 0;
}std::move() Ne Yapar?
std::move() aslında hiçbir şey taşımaz. Sadece bir lvalue'yu rvalue reference'a dönüştürür (cast). "Bu nesneyi artık kullanmayacağım, kaynaklarını alabilirsin" demenin yolu.
#include <vector>
#include <string>
#include <iostream>
int main() {
std::string a = "Merhaba Dunya";
// std::move olmadan — kopya
std::string b = a;
std::cout << "a: " << a << "\n"; // Merhaba Dunya — hâlâ sağlam
// std::move ile — taşıma
std::string c = std::move(a);
std::cout << "a: '" << a << "'\n"; // '' — boşaltıldı (unspecified ama valid)
std::cout << "c: " << c << "\n"; // Merhaba Dunya
return 0;
}Vector'de move
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> source = {"Ali", "Ayse", "Mehmet"};
// Tüm vector'ü taşı — O(1), eleman sayısı fark etmez
std::vector<std::string> dest = std::move(source);
std::cout << "source size: " << source.size() << "\n"; // 0
std::cout << "dest size: " << dest.size() << "\n"; // 3
for (const auto& name : dest) {
std::cout << name << " ";
}
std::cout << "\n";
return 0;
}⚠️ std::move sonrası nesne "valid but unspecified" durumda olur. Yani nesne hâlâ geçerli (destructor çağrılabilir, yeni değer atanabilir) ama içeriği belirsizdir. Move'dan sonra nesneyi kullanma — sadece yeni değer ata veya yok et.
Fonksiyon Parametrelerinde move
#include <vector>
#include <string>
#include <iostream>
class DataStore {
std::vector<std::string> items;
public:
// rvalue — taşı
void addItem(std::string item) {
items.push_back(std::move(item)); // item'ı vector'e taşı
}
void print() const {
for (const auto& item : items) {
std::cout << item << " ";
}
std::cout << "\n";
}
};
int main() {
DataStore store;
std::string name = "Ali";
store.addItem(name); // Kopya — name hâlâ geçerli
store.addItem("Ayse"); // rvalue — doğrudan taşınır
store.addItem(std::move(name)); // Taşı — name artık boş
store.print();
return 0;
}Rule of Five
Eğer sınıfın kaynak yönetimi yapıyorsa (raw pointer, file handle vb.), beş özel fonksiyonu birlikte tanımlamalısın:
class Resource {
int* data;
size_t size;
public:
// 1. Constructor
Resource(size_t n) : size(n), data(new int[n]{}) {}
// 2. Destructor
~Resource() { delete[] data; }
// 3. Copy constructor
Resource(const Resource& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// 4. Copy assignment
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
// 5. Move constructor
Resource(Resource&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 6. Move assignment (5'in parçası)
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};💡 Pratik ipucu: Smart pointer (
unique_ptr,shared_ptr) kullanıyorsan Rule of Five'a genellikle ihtiyacın olmaz — Rule of Zero ilkesine uy ve derleyicinin otomatik ürettiği fonksiyonlara güven.
Perfect Forwarding — std::forward
Template fonksiyonlarda bir argümanı olduğu gibi ileri aktarmak (forward) isteyebilirsin. Eğer argüman lvalue ise lvalue olarak, rvalue ise rvalue olarak iletisin. std::forward bunu sağlar.
#include <iostream>
#include <string>
#include <utility>
void process(const std::string& s) {
std::cout << "lvalue: " << s << "\n";
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << "\n";
}
// Universal reference (forwarding reference)
template<typename T>
void wrapper(T&& arg) {
// std::forward — arg'ın orijinal kategorisini korur
process(std::forward<T>(arg));
}
int main() {
std::string name = "Ali";
wrapper(name); // lvalue → lvalue overload çağrılır
wrapper(std::string("Ayse")); // rvalue → rvalue overload çağrılır
wrapper("Mehmet"); // rvalue → rvalue overload çağrılır
return 0;
}T&& bir template parametresinde kullanıldığında universal reference (veya forwarding reference) olur — hem lvalue hem rvalue kabul eder. std::forward<T> ise orijinal kategoriyi koruyarak argümanı iletir.
Detaylı perfect forwarding konusu ileri seviye C++ bilgisi gerektirir. Şimdilik şunu bil: std::forward, std::move'dan farklı olarak koşullu bir rvalue dönüşümü yapar — sadece argüman rvalue ise rvalue'ya dönüştürür.
Return Value Optimization (RVO)
Derleyici, fonksiyondan nesne dönerken kopyalama/taşımayı tamamen atlayabilir. Buna RVO (Return Value Optimization) veya NRVO (Named RVO) denir.
#include <vector>
#include <iostream>
std::vector<int> createVector() {
std::vector<int> result = {1, 2, 3, 4, 5};
// Burada kopyalama veya taşıma OLMAZ
// Derleyici result'ı doğrudan çağıran taraftaki değişkene oluşturur
return result;
}
int main() {
auto vec = createVector(); // RVO — sıfır kopya, sıfır move
for (int x : vec) std::cout << x << " ";
std::cout << "\n";
return 0;
}C++17'den itibaren bazı RVO durumları garanti altına alınmıştır (mandatory copy elision). Yani derleyici optimize etmek zorundadır.
#include <iostream>
struct Heavy {
Heavy() { std::cout << "Constructor\n"; }
Heavy(const Heavy&) { std::cout << "Copy\n"; }
Heavy(Heavy&&) { std::cout << "Move\n"; }
};
Heavy createHeavy() {
return Heavy(); // C++17: sadece Constructor çağrılır, copy/move yok
}
int main() {
Heavy h = createHeavy();
// Çıktı: sadece "Constructor" — ne copy ne move
return 0;
}⚠️ Return'da `std::move` kullanma!
return std::move(result)yazmak RVO'yu engeller. Derleyici zaten return edilen yerel değişkeni rvalue olarak değerlendirir.std::moveeklemek sadece zararlıdır.
// YANLIŞ — RVO'yu engeller
std::vector<int> bad() {
std::vector<int> v = {1, 2, 3};
return std::move(v); // Kötü! RVO iptal olur
}
// DOĞRU — RVO çalışır
std::vector<int> good() {
std::vector<int> v = {1, 2, 3};
return v; // RVO veya implicit move — derleyici en iyisini yapar
}Move Semantics Ne Zaman Faydalı?
Move semantics her yerde performans kazancı sağlamaz. En çok fayda sağladığı durumlar:
| Durum | Fayda |
|---|---|
| Büyük container'ları fonksiyonlar arası geçirmek | ✅ Çok büyük |
| String işlemleri | ✅ Büyük |
| unique_ptr transferi | ✅ Zorunlu (kopyalanamaz) |
| Küçük nesneler (int, double, Point) | ❌ Faydasız (kopya zaten ucuz) |
| Container'a push_back | ✅ emplace_back ile birlikte |
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> names;
std::string longName = "Bu cok uzun bir isim olabilir...";
// push_back(const T&) — kopya
names.push_back(longName);
// push_back(T&&) — move
names.push_back(std::move(longName));
// longName artık boş — ama names'te iki kopyası/taşınmışı var
// emplace_back — doğrudan oluştur
names.emplace_back("Dogrudan olusturuldu");
std::cout << "Size: " << names.size() << "\n";
return 0;
}Özet
lvalue bellekte adresi olan ifadedir (değişkenler), rvalue geçici ve isimsiz ifadedir (literal'ler, fonksiyon dönüşleri). Adresi alınabiliyorsa lvalue, alınamıyorsa rvalue.
Rvalue reference (`&&`) sadece rvalue'lara bağlanır ve move semantics'in temelini oluşturur.
Move constructor ve move assignment nesne kaynaklarını kopyalamadan, sadece pointer'ları el değiştirerek taşır — özellikle büyük nesnelerde dramatik performans kazancı sağlar.
`std::move()` hiçbir şey taşımaz, sadece lvalue'yu rvalue'ya dönüştürür (cast). "Bu nesneyi artık kullanmayacağım" mesajını verir.
`std::forward` template fonksiyonlarda argümanın orijinal değer kategorisini koruyarak iletir (perfect forwarding).
RVO (Return Value Optimization) fonksiyondan nesne dönerken kopyalama/taşımayı atlar. Return'da
std::movekullanma — RVO'yu engeller.
AI Asistan
Sorularını yanıtlamaya hazır