Taşıma Semantiği ve R-değerleri
Coddy'nin C++ Journey'sinin Nesne Yönelimli Programlama bölümünün bir parçası — ders 83 / 104.
C++'da her ifade ya bir lvalue (kalıcı bir kimliğe sahiptir, adreslenebilir) ya da bir rvalue'dur (geçicidir, yok edilmek üzeredir). Bu ayrımı anlamak, gereksiz kopyalamayı önleyen güçlü bir optimizasyon olan taşıma semantiğinin (move semantics) kapılarını açar.
&& ile bildirilen bir rvalue referansı, özellikle geçici nesnelere bağlanır. Bu, zaten yok olmak üzere olan nesnelerden kaynakları "çalmanıza" olanak tanır:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Taşıma kurucusu - kaynakları çalar
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Kaynağı geçerli bir durumda bırak
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Taşıma kurucusunu tetikler
}std::move fonksiyonu aslında hiçbir şeyi taşımaz; sadece bir lvalue'yu bir rvalue referansına dönüştürerek nesnenin kaynaklarından vazgeçmeye hazır olduğunuzu belirtir. Asıl taşıma işlemi, taşıma kurucusunda (move constructor) veya taşıma atama operatöründe (move assignment operator) gerçekleşir.
Taşıma semantiği (move semantics), konteynerler veya dizeler gibi kaynak yoğunluklu nesnelerle çalışırken performansı önemli ölçüde artırır. Megabaytlarca veriyi derinlemesine kopyalamak yerine, boyuttan bağımsız olarak sabit zamanlı bir işlem olan işaretçi (pointer) sahipliğini devredersiniz.
Görev
KolayTaşıma semantiğinin (move semantics) işleyişini gösteren, kaynak yöneten bir DataBuffer sınıfı oluşturalım. Dinamik olarak ayrılmış belleğin sahipliğini aktarırken, kaynakları kopyalamak yerine taşımanın verimliliği nasıl önemli ölçüde artırabildiğini göreceksiniz.
Kodunuzu üç dosya halinde düzenleyeceksiniz:
DataBuffer.h: Dinamik olarak ayrılmış bir tam sayı dizisini yönetenDataBuffersınıfınızı tanımlayın.Sınıfınızın veri işaretçisi (
int*), boyutu (size_t) ve işlemler sırasında hangi tamponun hangisi olduğunu takip etmeye yardımcı olacak bir isim (std::string) için özel (private) üyeleri olmalıdır.Şunları bildirin:
- Bir
std::stringisim vesize_tboyut alan, diziyi tahsis eden ve şunu yazdıran bir yapıcı (constructor):[name] constructed with size [size] - Bir rvalue referansı alan, kaynakları çalan ve şunu yazdıran bir taşıma yapıcısı (move constructor):
[name] moved from [source_name](burada taşınan tampon, kaynağın ismini alır) - Şunu yazdıran bir yıkıcı (destructor):
[name] destroyed(veya tampon taşınmışsaempty destroyed) - Mevcut boyutu döndüren bir
getSize()metodu - Tamponun ismini döndüren bir
getName()metodu
Taşıma yapıcınızı
noexceptolarak işaretlemeyi ve kaynak nesneyi geçerli bir boş durumda (nullptr, boyut 0, isim "empty") bırakmayı unutmayın.- Bir
DataBuffer.cpp: Başlık dosyanızda bildirilen tüm metodları uygulayın. Yıkıcı çalıştığında, veriyi yalnızca işaretçi null değilse silin. Çıktı için<iostream>kütüphanesini dahil edin.main.cpp: İki girdi okuyun:- Tamponunuz için bir isim (string)
- Tamponunuz için bir boyut (integer)
Verilen isim ve boyutta bir
DataBufferoluşturun. Ardındanstd::move()kullanarak birinciden taşıyarak ikinci bir tampon oluşturun. Taşıma işleminden sonra her iki tamponun durumunu yazdırın:Original: [name] size=[size]New: [name] size=[size]
std::moveiçin<utility>kütüphanesini dahil edin.
Örneğin, Alpha ve 100 girdileriyle:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedBuffer ve 50 girdileriyle:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedTaşıma yapıcısının, herhangi bir veriyi kopyalamadan ayrılmış belleğin sahipliğini nasıl aktardığına dikkat edin. Orijinal tampon boş ama geçerli bir durumda bırakılır ve programın sonunda her iki tampon da yok edildiğinde, yalnızca belleğe hala sahip olan tampon belleği gerçekten siler.
Kopya kağıdı
C++ ifadeleri ya lvalue (kalıcı, adreslenebilir) ya da rvalue (geçici) şeklindedir. Taşıma semantiği (move semantics), kaynakları kopyalamak yerine geçici nesnelerden aktararak performansı optimize eder.
Rvalue Referansları
&& ile bildirilen rvalue referansları, geçici nesnelere bağlanır:
Buffer(Buffer&& other) // Rvalue referans parametresiTaşıma Yapıcısı (Move Constructor)
Geçici bir nesneden kaynakları çalar. Bunu noexcept olarak işaretleyin ve kaynağı geçerli bir boş durumda bırakın:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Kaynağı geçerli bırak
other.size = 0;
}std::move
Bir lvalue'yu rvalue referansına dönüştürerek kaynakları aktarma isteğini belirtir. <utility> kütüphanesini dahil edin:
Buffer b1(1000);
Buffer b2(std::move(b1)); // Taşıma yapıcısını tetiklerstd::move kendi başına hiçbir şeyi taşımaz; asıl aktarım, taşıma yapıcısında veya taşıma atama operatöründe gerçekleşir.
Avantajlar
Taşıma semantiği, nesne boyutundan bağımsız olarak sabit zamanlı kaynak aktarımı sağlar ve büyük veri yapılarının maliyetli derin kopyalamalarından kaçınır.
Kendin dene
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Verilen name ve size ile bir DataBuffer oluşturun
// TODO: std::move() kullanarak birinciden taşıyarak ikinci bir tampon oluşturun
// TODO: Her iki tamponun durumunu yazdırın:
// Orijinal: [name] size=[size]
// Yeni: [name] size=[size]
return 0;
}
Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.
Nesne Yönelimli Programlama bölümündeki tüm dersler
1OOP Temelleri
Harici DosyalarC++ Build ve DerlemeBaşlık Dosyaları ve Kaynak DosyalarıAd Alanları ve KapsamC++'ta OOP'ye GirişSınıflar ve Nesneler'this' İşaretçisiMetotlar (Üye Fonksiyonlar)Öznitelikler (Veri Üyeleri)Ctor ve Dtor TemelleriÖzet - Basit Hesap Makinesi4Sınıf Özellikleri
Örnek ve Statik ÜyelerGetter ve Setter MetotlarıConst Üye FonksiyonlarMutable Anahtar KelimesiStatik Metotlar ve DeğişkenlerFriend Fonksiyonlar ve SınıflarÖzet - Banka Hesabı Yöneticisi7Kalıtım
Temel KalıtımKalıtım Erişim SeviyeleriCtor ve Dtor Çağrılma SırasıMetot Geçersiz KılmaSanal Fonksiyonlar ve VTableÇoklu KalıtımSanal KalıtımÖzet - Çalışan Hiyerarşisi10STL Genel Bakış
STL Genel Bakış ve FelsefesiSTL KonteynerleriİteratörlerSTL AlgoritmalarıFunctor'lar ve Lambda İfadeleriÖzet - Kelime Frekansı13Tasarım Kalıpları 1. Bölüm
Tasarım Kalıplarına GirişSingleton KalıbıFactory ve Abstract FactoryBuilder KalıbıObserver KalıbıStrategy Kalıbı2Bellek Yönetimi
Stack ve Heap Bellekİşaretçiler ve ReferanslarDinamik Bellek (new/delete)C++'ta Akıllı İşaretçilerC++'ta RAIIÖzet - Dinamik Dizi Yöneticisi5Kapsülleme
C++'da Erişim BelirleyicilerDerinlemesine Erişim BelirleyicilerBilgi GizlemeStruct vs Classİç İçe ve Dahili SınıflarÖzet - Öğrenci Kayıt Sistemi8Çok Biçimlilik
Derleme ve Çalışma Zamanı Çok BiçimliliğiFonksiyon Aşırı YüklemeSanal Fonksiyonlara Yeniden BakışSaf Sanal FonksiyonlarSoyut SınıflarC++'ta Arayüz TasarımıDynamic Casting ve RTTIÖzet - Şekil Hesaplayıcı3Yapıcılar ve Yıkıcılar
Varsayılan YapıcıParametreli YapıcıKopya YapıcıTaşıma YapıcısıYapıcı İlklendirme ListeleriTemsilci YapıcılarYıkıcılara Derinlemesine BakışÜç / Beş / Sıfır KuralıÖzet - String Sınıfı6Operatör Aşırı Yükleme
Operatör Aşırı Yüklemeye GirişAritmetik Operatör Aşırı YüklemeKarşılaştırma Operatörü Aşırı YüklemeStream OperatörleriAtama Operatörü Aşırı Yükleme[] ve () Operatör Aşırı YüklemeTip Dönüşüm OperatörleriÖzet - Matris Sınıfı9Şablonlar
Fonksiyon ŞablonlarıSınıf ŞablonlarıŞablon ÖzelleştirmeVariadic ŞablonlarSFINAE ve Type Traits TemelleriÖzet - Generic Konteyner12Modern C++ Özellikleri
Taşıma Semantiği ve R-değerleriMükemmel YönlendirmeDerinlemesine Lambda İfadeleristd::function ve std::bindconstexpr ve constevalYapılandırılmış Bağlamalaroptional, variant, any