← Kursa Dön
📄 Text · 15 min

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ık T&& 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::move eklemek 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:

DurumFayda
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::move kullanma — RVO'yu engeller.