Evet bir diğer Modern C++ 11 ile yazımız ile tekrar birlikteyiz. Bu yazımızda C++ 11 ile gelen önemli değişikliklerden biri olan “Move Semantics” yani Taşıma Semantiklerinden sizlere bahsedeceğim. Bu konu da derya deniz bir konu. Bu yazımda sadece önemli hususların üzerinden geçip, genel mantığı sizlere aktarıp, kalanını naçizane sizlere bırakacağım 🙂 Daha lambda’lar var gardaşım ne yapam yani.
Taşıma semantiklerinden bahsetmeden önce beyin kaslarımızı ısındırma ve meraklandırma adına, bu konu ile ilgili bir takım kavramları aşağıda sıralamaya çalıştım, yazımda bunlara teker teker değineceğiz. Nedir bunlar:
- rvalue referansları
- mükemmel yönlendirme “perfect forwarding” ve std::forward
- std::move
- taşıma oluşturucuları (“move constructor”) ve taşıma atama operatörleri (“move assignment operator”)
İçerik
Giriş
Taşıma semantiğinin arkasında yatan en önemli motivasyon arkadaşlar performans. Şöyleki C++ da metotlara parametre geçirme yöntemlerine baktığımız zaman elimizde bir kaç seçeneğimiz var nedir bunlar:
- “Pass by value”: Bu durumda ise ilgili değerin kendisi metoda kopyalanarak geçirilir ve metot içerisinde bu parametreye yapılan değişiklikler ilgili değere yansıtılmaz. Fazladan bir kopyalama yapıldığı için büyük veri yapılarında performans kaybına yol açar.
- “Pass by reference” : Yani parametrelerin referans (&) operatörü kullanılarak metotlara geçirilmesi. Bu durumda ilgili parametrelerin sadece adresinin (kendisi değil) kopyası tutulur. Bu bir nebze işaretçi kullanılarak parametrelerin geçirilmesine benzer ve metot içerisinde yapılan değişiklikler ilgili parametreleri değiştirir. Parametrenin kendisi kopyalanmaz.
Peki burada “Pass by value” durumundaki kopyalama yapmadan bir şekilde ilgili değeri taşıyamaz mıyız? Bu sayede gereksiz kopyalama masrafından da kurtulabiliriz. Burada kopyalama ile taşıma işlemi arasında en önemli fark, kopyalama durumunda kaynak hiç bir şekilde değiştirilmez bu operasyondan etkilenmez. Taşıma durumunda ise kaynağa göre bu durum değişebilir.
Taşıma semantiğinin temelinde yatan şey ilgili kaynağın sahipliğinin bir nesneden başka bir nesneye kopyalama yapılmadan ‘taşınması‘ / aktarılması olarak özetlenebilir.
Şimdi buraya kadar gördüklerimizi bir kaç örnek ile pekiştirmeye çalışalım. Örneğin bir metodumuz olsun ve bu metodun büyük bir nesne döndüğünü düşünelim. Normal şartlarda bunu iki şekilde yapabiliriz. Birincisi işaretçiler kullanarak bunu dönebiliriz:
1 2 3 4 5 6 7 8 9 10 11 |
vector<int>* MakeBigVector() { vector<int>* result = new vector<int>(1024); for(int i=0; i<1024; i++) { result[i] = rand(); } return result; } ::: vector<int>* v = MakeBigVector(); |
Ya da ilgili nesneyi “pass-by-reference” yöntemi ile geçirebiliriz.
1 2 3 4 5 6 7 8 9 10 11 |
void MakeBigVector(vector<int>& out) { out.resize(1024); for(int i=0; i<1024; i++) { out[i] = rand(); } } ::: vector<int> v; MakeBigVector(v); |
İşte taşıma semantiği ile artık bunu aşağıdaki gibi de yapabiliyoruz (Tabi burada std::vector varsayılan olarak bu taşıma semantiğini desteklediğinden bunu yapabiliyoruz 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 |
vector<int> MakeBigVector() { vector<int> result; for(int i=0; i<1024; i++) { result[i] = rand(); } return result; } ::: // C++ 11 de bu iş için kopyalama yapılmaz ama öncekilerde bütün değerler kopyalanırdı vector<int> v = MakeBigVector(); |
Gördüğünüz gibi C++03 kullanımından hiç bir farkı yok. Bunun en önemli artısı bu ve benzeri dil ve STL tarafından taşıma mantığı eklenmiş olan veri yapıları kullanan kodlarınız otomatikmen taşıma mantığını kullanır hale gelmiş olacak.
Benzer şekilde bir metottan ilgili nesnelerin dönülmesinde veya parametre olarak geçirilmesi durumunda taşıma mantığını kullanmak için aşağıdaki koşulların sağlanması gerekmekte:
- nesne rvalue olmalı,
- ilgili nesneye ilişkin sınıfta özel üyem taşıma metotlarının tanımlanmış olmalı (ileride taşıma oluşturucu ve kopya oluşturucu başlıklarına bakabilirsiniz).
Bundan sonra anlatacaklarımın hepsi bu temel ilke üzerine kurulu.
rvalue Referansları
rvalue referanslarına geçmeden önce lvalue’ler rvalue’lere bir bakmamız iyi olacak.
- lvalue: Bellekte bir yer tutan ve adresi temin edilen değer/ifade (“expression”).
- rvalue: Herhangi bir tanımlaması olmayan ve sadece hesaplama sırasında var olan ve kullanılan değer/ifade. Genelde C++ ve C kaynaklarında lvalue olmayan bütün ifadeler
- Daha önceleri & operatörü sadece lvalue’ler için kullanılabilmekteydi. Taşıma semantiği ile artık && operatörü de rvalue’ler için kullanılmakta.
Şimdi bir kaç örnek ile bunların kullanımına bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Burada a bir lvalue ve 1 ise rvalue'dür int a = 1; ... // Bu metot yerel bir değişken döndüğü için, döndüğü değer rvalue'dür string getName() { string s = "Hello world"; return s; } int main() { // rvalue'yu referans operatörü ile kullanamazsınız // error: cannot bind non-const lvalue reference of type 'std: bla bla hatası alınır string& deneme = getName(); // Problem yok. rvalue referans operatörü bunun için dostum // Kopyalama yok string&& deneme2 = getName(); // C++ 03 de kopyalama yapılıyor // C++ 11 de yapılmaz string deneme = getName(); } |
Mükemmel Yönlendirme “Perfect Forwarding”
Mükemmel yönlendirme özellikle fonksiyon şablonları için rvalue referanslı parametrelere geçirilmesi durumu için kullanışlı bir mekanizma. Eğer geçirilen parametre tipi rvalue referansı ise ve de geçirilen parametre lvalue ise bu parametreye lvalue referans gibi davranılıyor (yani ilgili parametreye yapılan değişiklikle değerini değiştiriyor. Bknz. “pass-by-reference”), başka tip bir veri geçirilir ise temel bir tip olarak davranılıyor. Şimdi bu durumları aşağıdaki örnekler ile inceleyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<typename T> void Foo(T&& t); int main() { X x; // 1) Bu durumda geçirilen parametre lvalue olduğu için T burada X& olarak kabul edilir FOO(x); // 2) Bu durumda geçirilen parametre geçici bir nesne olduğu için rvaluedur ve bu durumda da // T X olarak kabul edilir. FOO(X()); // 2 } |
Bu sayede lvalue ve rvalue referansları için ayrı ayrı metotlar yazmanıza gerek kalmaz. Bunun ile birlikte herhangi metoda geçirilen lvalue/rvalue parametreleri bu yapıları korunarak başka metotlara geçirilebilir. Bunun için de std::forward metodu kullanılır. İşte bu olaya Mükemmel Yönlendirme denir. Bu sayede gereksiz kopyalamaların önüne geçilir ve gereksiz lvalue/rvalue metotlarının fazladan yazılmasına ihtiyaç kalmaz. Yukarıdaki örneğin devamı niteliğinde olan aşağıdaki kullanıma bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// A) rvalue referansı alan metot void Goo(X&& t); // B) lvalue referansı alan metot void Goo(X& t); template<typename T> void Foo(T&& t) { // Gelen T tipine göre ilgili Goo metodu otomatik olarak belirlenir Goo(std::forward<T>(t)); } // Benzer şekilde rvalue referansı kullanan başka bir metot void Hoo(X&& t) { Goo(t); } int main() { X x; Foo(x); // 1 Foo(X()); // 2 Hoo(x); Hoo(X()); // 3 } |
Yukarıda Foo metodu içerisinde hangi Goo’nun çağrılacağı otomatik olarak belirlenir.
1 ile işaretlenen satırda geçilen tip lvalue olduğu ve std::forward kullanımı sonucu B ile yorumlanan Goo metot (lvalue referans kullanan) çağrısı gerçekleştirilir.
2 ile işaretlenen satırda geçilen tip rvalue olduğu ve std::forward kullanımı sonucu A ile yorumlanan Goo metot (rvalue referans kullanan) çağrısı gerçekleştirilir.
Burada unutulmaması gereken husus bu kullanım metot şablonları için geçerlidir.
Taşıma Oluşturucusu (“move constructor”) ve Taşıma Atama Operatörü (“move assignment operator”)
Evet yazımın başında da bahsettiğim üzere sınıflarınıza taşıma kabiliyeti kazandırmak için:
- Taşıma Oluşturucusu (“Move constructor”)
- Genel format =>
C::C(C&& other);
- Genel format =>
- Taşıma Atama Operatörü (“Move Assignment Operator”)
- Genel format =>
C& C::operator=(C&& other);
- Genel format =>
tanımlamanız gerekmekte. Tabi çoğu derleyici basit sınıflarımız için yine bu metotları sağ olsun bizler için tanımlar 🙂 Ama velev ki karmaşık bir sınıfımız var ya da kopya oluşturucu veya atama metodu tanımladık veya biz heyecan arıyoruz. Benzer şekilde default ya da delete anahtar kelimelerini bu özel metotlar için de kullanabilirsiniz.
O durumda da aşağıdaki örneği kendinize referans alabilirsiniz efenim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#include <iostream> #include <algorithm> class A { public: // Basit oluşturucu ile kaynakların ilklendirilmesi explicit A(size_t length) : mLength(length), mData(new int[length]) { std::cout << "A(size_t). length = " << mLength << "." << std::endl; } // Yokedici :) ~A() { std::cout << "~A(). length = " << mLength << "."; if (mData != NULL) { std::cout << " Deleting resource."; delete[] mData; // Delete the resource. } std::cout << std::endl; } // Kopya oluşturucu A(const A& other) : mLength(other.mLength) , mData(new int[other.mLength]) { std::cout << "A(const A&). length = " << other.mLength << ". Copying resource." << std::endl; std::copy(other.mData, other.mData + mLength, mData); } // Kopya atama operatörü A& operator=(const A& other) { std::cout << "operator=(const A&). length = " << other.mLength << ". Copying resource." << std::endl; if (this != &other;) { delete[] mData; // Free the existing resource. mLength = other.mLength; mData = new int[mLength]; std::copy(other.mData, other.mData + mLength, mData); } return *this; } // Taşıma oluşturucusu A(A&& other) : mData(NULL) , mLength(0) { std::cout << "A(A&&). length = " << other.mLength << ". Moving resource.\n"; // Gelen veri işaretçisinden ilgili adresi ve boyutu al mData = other.mData; mLength = other.mLength; // Kaynak işaretçi tarafından adreslenen kaynağı salıver. // Bunu yapmaz isek bellek birden fazla sayıda boşaltılmaya çalışılır other.mData = NULL; other.mLength = 0; } // Taşıma atanma operatörü A& operator=(A&& other) { std::cout << "operator=(A&&). length = " << other.mLength << "." << std::endl; if (this != &other) { // Mevcut verileri boşalt delete[] mData; // Gelen veri işaretçisinden ilgili adresi ve boyutu al mData = other.mData; mLength = other.mLength; // Kaynak işaretçi tarafından adreslenen kaynağı salıver. // Bunu yapmaz isek bellek birden fazla sayıda boşaltılmaya çalışılır other.mData = NULL; other.mLength = 0; } return *this; } // Verinin boyutu dönülür size_t Length() const { return mLength; } private: // Verinin boyutu size_t mLength; // Veri int* mData; }; |
std::move Metodu
std::move metodunun aslında yaptığı çok uçuk kaçık bir şey yok. Tek yaptığı lvalue ya da rvalue bir parametre alıp herhangi bir kopya oluşturucu çağırmadan rvalue olarak bu parametreyi dönmektir. Aşağıda örnek olabilecek bir kullanım gösterilmiştir.
1 2 3 4 5 6 |
template <class T> typename remove_reference<T>::type&& move(T&& a) { return a; } |
Evet bir yazımızın daha sonun geldik. Aslında bu yazı ile birlikte C++ 11 e dair lambda’lar dışında büyük bütün yenilikleri tamamladık. Bunlar dışında STL’de gelen bir takım yenilikler var ama onlara da bir yazı ile bakarız. Haydı kalın sağlıcakla.
Kaynaklar
C++11/C++14 5. RVALUE REFERENCE AND MOVE SEMANTICS
C++ Rvalue References Explained
Ten C++11 Features Every C++ Developer Should Use
Merhaba, öncelikle yazı için teşekkürler, elinize sağlık. 2 tane sorum olacak;
– Mükemmel Yönlendirme başlığındaki ilk kodda FOO(X()) için;
“// 2) Bu durumda geçirilen parametre geçici bir nesne olduğu için lvaluedur”
demişsiniz.Burada geçici nesne olduğu için rvalue olması gerekmiyor mu ?
– Mükemmel Yönlendirme başlığındaki ikinci kodda
“Foo(X());” için “geçilen tip rvalue” demişsiniz.
Bence de bu rvalue ancak aşağıdaki sorguyu boş bir X sınıfı için yapınca false geliyor, neden false gediğini anlayamadım
class X {} ;
std::is_rvalue_reference
Öncelikle geç cevap için çok özür diliyorum. Site bir şekilde, bildirim üretmemiş. Neyse sorularınıza gelecek olursak:
1) Haklısınız hemen düzeltiyorum 🙂
2) Ben aşağıdaki kodu VS 2017 de denediğimde true değeri alıyorum. Burada std::is_rvalue_reference geçirdiğiniz tipten dolayı olabilir mi, çonkü X ve X& tipleri için bu metot false dönecektir, X&& için ise true dönüyor.
std::cout << std::is_rvalue_reference::value << '\n';
Umarım yardımcı olmuşumdur.
Teşekkürler.
Önceki mesajımda tam çıkmamış ben sorguyu fonksiyona geçirilen default ctor için yapmıştım. X&& için belirttiğiniz gibi true geliyor ama X() için false geliyor. Kodu “http://cpp.sh/” üzerinde çalıştırdım.
std::cout << std::is_rvalue_reference::value << '\n';
Evet dediğiniz gibi yorumlarda ilgili kod parçaları görünmüyor, o sebeple net bir şey diyemedim. is_rvalue_reference ilgili tipin rvalue olup olmadığını sorguluyor, bu anlamda default ctor ile bu kontrolü nasıl yaptığınızı tam olarak anlayamadım.
Örneğe gelecek olursak, 2. durumda bir kopyalama yapılıyor (bundan yazıda bahsetsem iyi olurdu), yani geçici bir nesne oluşturuluyor (ve bu sebeple rvalue) ve void Goo(X&& t) imzalı metot çağrılıyor. Ama ilk durumda yani zaten oluşturulmuş x nesnesini geçirmeye kalktığınızda ise bu lvalue olarak ele alınıp void Goo(X& t) imzalı metot çağrılıyor. Diyelim ki, void Goo(X&& t) imzalı metodu hiç tanımlamadınız ve bunun yerine void Goo(X t) metodunu tanımladınız, bu durumda da derleme hatası alacaksınız, çünkü derleyici void Goo(X& t) mu void Goo(X t) mu cağıracağına karar veremeyecek.
Örnek de biraz bunu göstermeye yönelikti, benzer durum referans sayfasında da anlatılıyor: https://en.cppreference.com/w/cpp/types/is_rvalue_reference. Umarım açıklayıcı olmuştur.
Hocam 2. durumdaki rvalue geçirilen Foo’dan Goo(X&& t)’nin çağırdığını anladım da o Foo’ya geçirilen “X()” ifadesi neden sorguda false geliyor onu anlayamadım.
“is_rvalue_reference” sorgusunu “X()” ifadesi ile çağırıyorum.
Alttaki linke sorgu fotosunu koydum, 4. durumda false geliyor, neden true gelmiyor acaba ?
https://drive.google.com/file/d/1ueRSU4eVFWxphIwfmkwEJI2EPij0JnjD/view?usp=sharing
Tamamdır, şimdi net oldu durum hocam. Hocam bana kalırsa sıkıntı std::is_rvalue_reference<> içerisinde geçirdiğiniz X() den kaynaklanıyor. Sonuçta is_rvalue_reference sizden bir tip bekliyor siz bir nesne geçiriyorsunuz. Keza aynı kod içerisinde std::is_rvalue_reference yerine std::is_lvalue_reference da kullansanız yine false dönüyor.
Merhaba
vector result = new vector(1024);
Burası kodlamada hata veriyor
Merhabalar, ilgili kod yanlış olmuş, yazıda düzelttim. Doğrusu:* result = new vector (1024);
vector
olmalıydı. Geri bildirim için teşekkür ediyorum.