---
title: 'Taşıma Semantiği'
source: 'https://academia.sh/tr/kurslar/cpp-standart-kutuphane/tasima-semantigi'
course: 'Kaynak Yönetimi ve Standart Kütüphane'
language: tr
updated: '2026-09-26T03:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'sağ değer, sol değer ve sağ değer referansı arasındaki farkı ayırt eder'
    - 'std::move çağrısının bir tip dönüşümünden ibaret olduğunu gösteren bir örnek kurar'
    - "taşınmış bir std::string'in geçerli ama belirtilmemiş durumda kaldığını doğru biçimde ele alır"
    - 'kaynağın yıkıcısının taşımadan sonra da çalıştığını basılan bir satırla kanıtlar'
---

# Taşıma Semantiği

std::move bir tip dönüşümünden ibarettir; asıl taşımayı yapan taşıyan yapıcı ve taşıyan atama işlecidir. Bir std::string taşındıktan sonra kaynakta ne kaldığını ölçme.

Önceki ders taşınan bir `unique_ptr`'ın kaynağını standardın verdiği bir sözle boşalttığını gösterdi: taşıma bitince kaynak `nullptr` olur, bu güvence akıllı işaretçinin kendi türünden gelir. Ama her tür aynı kesinlikte konuşmaz. `unique_ptr` yalnızca bir işaretçi taşıdığı için boşluğu ucuza garanti edebilir; `std::string` gibi kendi içinde veri taşıyan bir tür taşındığında standart aynı sözü vermez. Bu ders, standardın daha az kesin konuştuğu o sonucu ölçüyor.

## Sağ Değer, Sol Değer, Sağ Değer Referansı

Bir ifadenin sol değer (lvalue) mi sağ değer (rvalue) mi olduğu, adının olup olmamasından çok, o ifadenin adresinin alınıp alınamayacağıyla ilgilidir. `s` gibi adlandırılmış bir değişken bir sol değerdir; `std::string("gecici")` gibi geçici bir sonuç ya da `std::move(s)`'nin döndürdüğü ifade bir sağ değerdir. Sağ değer referansı (rvalue reference), `T&&` yazımıyla bildirilir ve yalnızca sağ değerlere bağlanabilir; bu bağlanma, derleyiciye "bu değer artık kimsenin ihtiyaç duymadığı geçici bir değer, içeriğini alabilirsin" sinyalini verir.

`std::move` bir işlev gibi görünür ama aslında hiçbir veriyi taşımaz; yaptığı tek şey, elindeki bir sol değeri bir sağ değer referansına dönüştürmektir — düz bir `static_cast<T&&>` çağrısından ibarettir. Bu dönüşüm tek başına hiçbir taşıyan yapıcı ya da taşıyan atama çağırmaz; yalnızca derleyicinin aşırı yükleme çözümlemesinde sağ değere özgü sürümü (varsa) seçmesine izin verir.

Bu yüzden `std::move(s)` ifadesinin kendisi hiçbir yan etki üretmez: `s`'nin türü değişmez, adresi değişmez, içeriği değişmez. Yan etki, bu ifade bir yapıcıya ya da atama işlecine argüman olarak verildiğinde ve derleyici o çağrı için taşıyan sürümü seçtiğinde ortaya çıkar. `std::move(s)` bir işlev çağrısının içine hiç girmeden, örneğin yalnızca bir değişkene atanıp bir daha kullanılmadan da yazılabilir — bu durumda hiçbir taşıma gerçekleşmez, çünkü hiçbir taşıyan yapıcı ya da taşıyan atama çağrılmamıştır. Adın "taşı" olması, çağrının kendisinin bir taşımayı garanti ettiği anlamına gelmez; yalnızca bir isteği, bir izni ifade eder.

Bir sınıf hem `T(const T&)` hem de `T(T&&)` sürümünü birlikte bildirdiğinde, hangisinin çağrılacağına derleyici argümanın değer kategorisine bakarak karar verir: adlandırılmış bir sol değer verilirse `const T&` sürümü, `std::move` ile üretilmiş ya da zaten geçici olan bir sağ değer verilirse `T&&` sürümü seçilir. Bu seçim çağıran tarafın hiçbir ek işaret vermesini gerektirmez — aynı çağrı yazımı (`T yeni(kaynak);`), `kaynak`'ın türüne göre farklı bir yapıcıya yönlenir. `T&&` parametresi olmayan bir sınıfta ise her iki durumda da tek seçenek olan `const T&` sürümü çalışır; sağ değer verilmiş olması bu durumda hiçbir fark yaratmaz, çünkü derleyicinin seçebileceği başka bir aşırı yükleme yoktur.

## Taşıyan Yapıcı ve Taşıyan Atama İşleci

Bir sınıfın taşıyan yapıcısı (move constructor) `T(T&& diger)` imzasıyla, taşıyan atama işleci (move assignment operator) `T& operator=(T&& diger)` imzasıyla bildirilir. İkisi de kaynaktaki kaynağı (bir arabellek, bir tutamaç, bir sayı) hedefe devretmek üzere yazılır; kopyalayan sürümlerinden farkı, yeni bir kaynak açmak yerine var olanı almalarıdır.

Aşağıdaki `Kutu` sınıfı bunu elle gösteriyor. Taşıyan yapıcı, `diger`'in `etiket_` üyesini kendi `etiket_` üyesine taşıyor — burada da `std::move` yalnızca `diger.etiket_`'i bir sağ değere çeviriyor, gerçek taşımayı `std::string`'in kendi taşıyan yapıcısı yapıyor. Taşıyan atama işleci aynı devri, zaten var olan bir nesnenin üzerine yazarak yapıyor. İkisi de `noexcept` işaretlidir; bu işaret yalnızca bir belge notu değildir — standart kütüphanenin `std::vector` gibi kapsayıcıları, yeniden ayırma sırasında elemanları taşıyıp taşıyamayacaklarına bu işarete bakarak karar verir. `noexcept` olmayan bir taşıyan yapıcı, kapsayıcıyı taşımak yerine kopyalamaya zorlayabilir.

```cpp
// kutu.cpp — tasiyan yapici, tasiyan atama ve kaynagin yikicisi
#include <cstdio>
#include <string>
#include <utility>
class Kutu {
public:
    explicit Kutu(std::string etiket) : etiket_(std::move(etiket)) {}
    Kutu(Kutu&& diger) noexcept : etiket_(std::move(diger.etiket_)) {
        std::printf("tasiyan yapici calisti\n");
    }
    Kutu& operator=(Kutu&& diger) noexcept {
        etiket_ = std::move(diger.etiket_);
        std::printf("tasiyan atama calisti\n");
        return *this;
    }
    Kutu(const Kutu&) = delete;
    Kutu& operator=(const Kutu&) = delete;
    ~Kutu() { std::printf("Kutu yikicisi calisti\n"); }
private:
    std::string etiket_;
};
static void tasiyan_islevler() {
    Kutu a("ilk etiket, kisa dize eniyilemesini asacak kadar uzun bir metin");
    Kutu b(std::move(a));
    Kutu c("ikinci etiket, yine kisa dize eniyilemesini asacak kadar uzun bir metin");
    c = std::move(b);
}
static void dize_tasima() {
    std::string s = "kucuk dize eniyilemesini asacak kadar uzun bir metin, tasima icin";
    std::string t = std::move(s);
    std::printf("t (kaynaktan gelen deger) = %s\n", t.c_str());
    s = "yeni deger";
    std::printf("s (yeni deger verildikten sonra) = %s\n", s.c_str());
}
static void sabit_dize() {
    const std::string c = "sabit bir metin, tasima yapicisina baglanamayacak kadar sabit";
    std::string once = c;
    std::string kopya = std::move(c);
    std::printf("kopya (c'den gelen deger) = %s\n", kopya.c_str());
    std::printf("c ayni mi: %s\n", (c == once) ? "evet" : "hayir");
}
int main() {
    tasiyan_islevler();
    dize_tasima();
    sabit_dize();
}
```

```text
tasiyan yapici calisti
tasiyan atama calisti
Kutu yikicisi calisti
Kutu yikicisi calisti
Kutu yikicisi calisti
t (kaynaktan gelen deger) = kucuk dize eniyilemesini asacak kadar uzun bir metin, tasima icin
s (yeni deger verildikten sonra) = yeni deger
kopya (c'den gelen deger) = sabit bir metin, tasima yapicisina baglanamayacak kadar sabit
c ayni mi: evet
```

**KY3.** `tasiyan_islevler` içindeki `Kutu` nesnelerinin hiçbiri, taşınmış olmaktan dolayı yıkıcısız kalmıyor: açılan her `Kutu` nesnesi kapsam sonunda kendi `Kutu yikicisi calisti` satırını basıyor. `a`'nın içeriği `b`'ye, `b`'nin içeriği `c`'ye taşındıktan sonra bile hepsi işlev sonunda normal biçimde yok ediliyor. Bu, önceki dersin M10/K02 ve M11/K02 karşılaştırmasını burada da doğruluyor: Rust'ta taşınan bir değişken derleyici tarafından artık kullanılamaz sayılır ve kapsam sonunda onun için hiçbir yıkım kodu çalışmaz; C++'ta taşınan nesne yaşamaya devam eder, yıkıcısı yine çağrılır — taşıma nesnenin ömrünü değil, yalnızca içeriğini etkiler.

Taşıyan atama işlecinin bir başka inceliği kendi kendine atamaya karşı davranışıdır: `x = std::move(x);` gibi bir çağrı yazılabilir olmalı ve nesneyi bozmamalıdır. `Kutu`'nun taşıyan atama işleci burada `etiket_ = std::move(diger.etiket_);` satırıyla `std::string`'in kendi taşıyan atamasına güveniyor; `std::string`'in kendi kendine taşıyan ataması güvenlidir: tanımsız davranışa yol açmaz ve nesneyi geçerli bırakır, ama standart dizgenin ardından hangi değeri taşıyacağını söylemez; sonuç bu dersin **belirsiz** sınıfına girer. Kaynağı elle yöneten bir sınıf (örneğin çıplak bir işaretçi tutan bir sınıf) yazarken bu kontrolü açıkça eklemek gerekir — aksi hâlde kendi kendine atama nesnenin kaynağını serbest bırakıp hemen ardından o serbest bırakılmış kaynağı okumaya çalışabilir.

## Kaynağın Yıkıcısı Neden Hâlâ Çalışır

`tasiyan_islevler` çıktısındaki her `Kutu yikicisi calisti` satırı rastgele bir ayrıntı değil; C++'ın taşıma modelinin doğrudan bir sonucudur. Bir nesne taşındığında kapsam içinde kalmaya devam eder — ne yaşam süresi kısalır ne de yıkıcı çağrısı atlanır. `a` içeriğini `b`'ye devrettikten sonra bile `a` hâlâ geçerli bir `Kutu` nesnesidir; kapsam sonuna geldiğinde derleyici onun için de normal yıkıcı çağrısını üretir. Yıkıcının kendisi taşınmış ya da taşınmamış olmak arasında bir ayrım yapmaz — `~Kutu()` her zaman aynı gövdeyi çalıştırır, çünkü sınıf yazarı yıkıcıyı yalnızca bir kez, nesnenin olağan sonu için yazar.

Bu davranışın güvenli olmasının koşulu, taşıyan yapıcının ve taşıyan atamanın kaynağı yıkıcının güvenle çalışabileceği bir duruma bırakmasıdır. `Kutu` burada `etiket_` üyesini `std::string`'e devrediyor; `std::string`'in kendi taşıyan yapıcısı, kaynak dizeyi geçerli ama belirtilmemiş bir duruma getirir ve bu durumun kendi yıkıcısı tarafından güvenle serbest bırakılabileceğini garanti eder. Eğer `Kutu` bir `std::string` yerine çıplak bir `char*` tutsaydı, taşıyan yapıcının kaynaktaki işaretçiyi `nullptr`'a çekmesi gerekirdi — aksi hâlde hem kaynak hem hedef aynı belleği serbest bırakmaya çalışır ve program çift serbest bırakma hatasına düşerdi. Bir taşıyan yapıcı yazmanın örtük kuralı budur: kaynağı, o kaynağın yıkıcısı ikinci kez zarar vermeden çalışabilecek bir duruma getirmek.

Bu, Rust'ın modeliyle tam burada ayrılıyor. Rust'ta taşınan bir değişken için derleyici kapsam sonunda hiçbir yıkım kodu üretmez — değişken artık "canlı" sayılmadığı için düşürme (drop) çağrısı ona hiç uygulanmaz. C++'ta ise böyle bir muafiyet yoktur: taşınan nesne kapsamın sonuna kadar yaşamaya devam eder ve yıkıcısı, tıpkı hiç taşınmamış bir nesneninki gibi, mutlaka çalışır. Bu yüzden C++'ta taşıma sonrası kaynağın hangi sınıfa (**aynen**, **değişti**, **boş**, **belirsiz**) düştüğünü bilmek önemlidir — o nesne ortadan kalkmaz, kodun geri kalanı onunla (ya da en azından onun yıkıcısıyla) hâlâ karşılaşacaktır.

## Taşınmış Bir `std::string`: Geçerli Ama Belirtilmemiş Durum

`dize_tasima` işlevindeki `std::string t = std::move(s);` satırından sonra `s`'nin içeriği hiçbir yerde okunmadan, doğrudan `s = "yeni deger";` ile üzerine yazılıyor. Bu sıralama rastgele değil: standart, taşınmış bir `std::string`'in "geçerli ama belirtilmemiş durumda" (valid but unspecified state) kalacağını söyler — nesne hâlâ geçerlidir, üzerinde `size()` çağırmak ya da yeni bir değer atamak güvenlidir, ama o an ne tuttuğu standart tarafından tanımlanmaz. Bu yüzden bu ders `s`'nin taşıma sonrası içeriğini hiçbir satırda basmıyor; yalnızca yeni bir değer verildikten sonraki hâlini okuyor.

`t`'nin değeri ise böyle değil: hedef nesnenin taşıma sonrası içeriği standart tarafından tam olarak tanımlıdır — kaynağın taşımadan önceki değeriyle aynı olacaktır. Çıktıdaki `t (kaynaktan gelen deger) = ...` satırı bu yüzden güvenle basılabiliyor; **belirsiz** sınıfına düşen yalnızca kaynak, hedef değil.

**KY4.** Taşımanın kopyalamadan ucuz olmasının nedeni de burada gizli: `std::string`'in taşıyan yapıcısı `t`'nin arabellek işaretçisini `s`'den devralır, karakterleri tek tek kopyalamaz. Bir kopyalayan yapıcı çağrılsaydı, kaynaktaki her karakter için yeni bir arabellek ayrılır ve o arabelleğe kaynaktan tek tek veri yazılırdı — dizenin uzunluğuyla orantılı bir işlem. Taşıyan yapıcı ise yalnızca birkaç üyeyi (arabellek işaretçisi, uzunluk, kapasite) hedefe kopyalar ve kaynağınkini sıfırlar; bu, dizenin içeriğinin ne kadar uzun olduğundan bağımsız, sabit maliyetli bir işlemdir. Bu ders bu devri saymaz — kaç karakterin taşındığı ya da kaç bayt kopyalanmadığı bu dersin ölçtüğü şey değil; ölçülen tek şey, işlem bitince kaynağın hangi sınıfa düştüğü.

Önceki kursun ölçtüğü "hangi nesne" sorusu (M11/K04) burada geçerli değil: orada `kopya` sınıfı bir hedefin adını taşıyordu, burada `aynen` bir kaynağın kendisinin adını taşıyor. Bir taşıma, hedefte her zaman kaynağın eski değerini kurar — hedef sınıfı buradaki ölçümün konusu değildir; ölçülen, geride kalan kaynaktır.

## Sınırlayıcı Ölçüm

`sabit_dize` işlevi bu dersin sınırını gösteriyor: `c` bir `const std::string` olduğu için `std::move(c)` çağrısı `const std::string&&` türünde bir sağ değer referansı üretir. `std::string`'in taşıyan yapıcısı `std::string&&` (sabit olmayan) bir parametre bekler; `const std::string&&` ona bağlanamaz. Derleyici bu durumda aşırı yükleme çözümlemesinde geriye düşer ve kopyalayan yapıcıyı çağırır. Sonuç: `c` bir taşıma gibi görünen bir çağrıdan sonra bile **aynen** kalır — çıktıdaki `c ayni mi: evet` satırı bunu doğruluyor.

Bu, `std::move` yazmanın bir taşımayı garanti etmediğini gösteren en net örnektir. `std::move` yalnızca bir tip dönüşümüdür; hangi yapıcının gerçekten çağrılacağına derleyicinin aşırı yükleme çözümlemesi karar verir, ve bir `const` nitelemesi bu çözümlemeyi taşıyan yapıcıdan uzaklaştırabilir. Pratikte bu, `const` bir değişkeni bir işleve taşımak niyetiyle geçirmenin genellikle bir tasarım hatası olduğu anlamına gelir: değişken `const` olarak kaldığı sürece derleyicinin taşıyan sürümü seçmesi mümkün değildir, kod her zaman kopyalayan sürüme düşer — bu yüzden bir kaynağın gerçekten taşınması isteniyorsa o kaynağın `const` olmaması gerekir.

## Özet

- Sol değer adlandırılmış, adresi alınabilen bir ifadedir; sağ değer geçici bir sonuçtur; sağ değer referansı (`T&&`) yalnızca sağ değerlere bağlanır.
- `std::move` bir tip dönüşümüdür, kendisi hiçbir veriyi taşımaz — hangi yapıcının çağrılacağına aşırı yükleme çözümlemesi karar verir.
- Taşınmış bir `std::string`'in kaynağı geçerli ama belirtilmemiş durumdadır (**belirsiz** sınıfı); bu ders onun içeriğini okumadan önce ona her zaman yeni bir değer verdi.
- Taşınan nesnenin yıkıcısı normal biçimde çalışmaya devam eder — C++'ta taşıma ömrü bitirmez, yalnızca içeriği devreder.
- `const` bir kaynak üzerinde `std::move` çağırmak taşıyan yapıcıya değil kopyalayan yapıcıya düşer; kaynak bu durumda **aynen** kalır — bu dersin sınırlayıcı ölçümü budur.

## Sonraki Adım

Bu ders bir taşımanın gerçekleşip gerçekleşmediğinin, `std::move` yazmakla değil hangi yapıcının fiilen çağrıldığıyla belirlendiğini gösterdi. Bir sınıfın taşıyan yapıcısının derleyici tarafından örtük olarak yazılıp yazılmayacağı da benzer bir çözümlemeye bağlıdır — ve bu çözümlemeyi sınıfın yazdığı başka bir özel üye işlev bile bozabilir. Sonraki ders, bir yıkıcı yazmanın örtük taşıyan yapıcıyı nasıl bastırdığını ve sıfır kuralıyla beş kuralının bunu nasıl yönettiğini ölçüyor.
