Modern C++ (5) : Taşıma Semantikleri

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”)

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:

Ya da ilgili nesneyi “pass-by-reference” yöntemi ile geçirebiliriz.

İş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 🙂

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:

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:

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:

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);
  • Taşıma Atama Operatörü (“Move Assignment Operator”)
    • Genel format => C& C::operator=(C&& other);

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.

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.

 

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

Lesson #5: Move Semantics

C++ Rvalue References Explained

Ten C++11 Features Every C++ Developer Should Use

Move semantics and rvalue references in C++11

A Brief Introduction to Rvalue References

8 Comments Modern C++ (5) : Taşıma Semantikleri

  1. Mehmet uluskan

    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

    Reply
    1. yazılımperver

      Ö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.

      Reply
      1. Mehmet uluskan

        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';

        Reply
        1. yazılımperver

          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.

          Reply
          1. yazılımperver

            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.

    1. yazılımperver

      Merhabalar, ilgili kod yanlış olmuş, yazıda düzelttim. Doğrusu:
      vector* result = new vector(1024);
      olmalıydı. Geri bildirim için teşekkür ediyorum.

      Reply

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.