Modern C++: std::move ve std::forward fonksiyon şablonları
Açılışı direk Scott Meyers’in Effective Modern C++ kitabında bu konu hakkında yazdıkları ile yapalım.
std::move hiçbir şey taşımamaktadır. std::forward hiçbir şey göndermemektedir. Çalışma zamanında ikisi de hiçbir şey yapmamaktadır. Tek bir byte bile işletilebilir kod üretmemektedirler.
Peki o zaman bu fonksiyon şablonları ne işe yaramaktadırlar?
Rvalue referansları modern C++ ile hayatımıza girmiştir ve Rvalue referanslarının dile eklenmesinin iki büyük nedeni vardır. Bunlardan biri taşıma semantiği (move semantics) diğeri ise mükemmel gönderim (perfect forwarding) idir.
Taşıma semantiği derleyicilerin yüksek maliyetli kopyalama işlemleri yerine daha az maliyetli olan taşıma ile işlerini yapabilmesini mümkün kılınmaktadır. Örneğin Modern C++ öncesi STL kaplarına (container) bir veri eklemenin tek yolu kopyalama semantiği iken Modern C++ ile kopyalama semantiğine ek olarak taşıma semantiği ve mükemmel gönderim ile de veri ekleyebilmemiz mümkündür.
Mükemmel gönderim ise bize fonksiyon şablonları aracılığı ile fonksiyon argümanlarının başka bir hedef fonksiyona değer kategorilerinde ve türlerinde herhangi bir değişikliğe uğramadan doğrudan gönderilebilmesine imkan vermektedir.
std::move ve std::forward taşıma semantiği ve mükemmel gönderim senaryolarında bizim niyetimizi açıkça gösteren ve aslında sadece tür dönüşümü sağlayan fonksiyon şablonlarıdır. std::move aldığı nesneyi koşulsuz olarak Rvalue değer kategorisine dönüştürken std::forward da bazı koşullar sağlandığında bu dönüşümü yapmaktadır.
std::move
std::move argüman olarak aldığı nesneyi herhangi bir koşula bakmadan Rvalue değer kategorisine dönüştürerek geri döndürmektedir. Bu semantik bir zorunluluk olmasa da kodu okuyan kişi ilgili nesnenin taşıma semantiği amacıyla kullanıldığını ve artık bu nesneye taşımadan sonraki kapsamda ihtiyaç duyulmadığını belirtmektedir.
Fonksiyon şablonuna bakarsak std::move fonksiyon şablonu universal referans/forwarding referans ile gösterilen bir nesne almaktadır ve yine aynı nesneyi gösteren bir referans geri döndürmektedir. T&& her zaman Rvalue referans anlamına gelmemektedir. Burada T&& universal referans olarak kullanılmaktadır.
Buradaki mantık ise “Reference Collapsing” kurallarına dayanmaktadır. Bu kuralları aşağıda mümkün olduğunca belirtmeye çalıştım ve bir tablo olarak paylaştım. Bu tablodan şu şekilde bir çıkarım yapmamız iyi olacaktır. std::move fonksiyonuna geçilen argüman eğer Lvalue değer kategorisinde ise tür çıkarımı Lvalue referans olarak, Rvalue değer kategorisinde ise tür çıkarımı Rvalue referans olarak yapılır.
Bu yüzden de argümanın değer kategorisi ne olursa olsun bizim her şekilde Rvalue değer kategorisine dönüştürebilmemiz için std::remove_reference_t type_traits aracı kullanılmaktadır ve verilen argümanın değer kategorisinden bağımsız olarak aynı nesneyi Rvalue değer kategorisine dönüştürürek geri döndürmüş oluyoruz.
Yani std::move fonksiyonuna baktığımızda artık bu fonksiyon şablonunun universal referans aldığını ve geri dönüş değerinin de Rvalue referans olduğunu yani Rvalue değer kategorisinin garanti edildiğini anlayabilmekteyiz.
Diğer bir özellik ise yine koddan anlayabileceğimiz üzere std::move bir constexpr fonksiyon şablonudur (C+14 ve sonrası) ve bu yüzden çalışma zamanında herhangi bir maliyeti bulunmamaktadır. Tür dönüşüm işlemi derleme zamanında yapılmaktadır.
Bu fonksiyon şablonları için ayrıca exception safety garantisi olarak nothrow garantisi verilmiştir.
std::remove_reference / std::remove_reference_t
type_traits kütüphanesi içerisinde yer alan std::remove_reference hem std::move hem de std::forward gerçekleştiriminde kullanıldığı için bu sınıf şablonunun gerçekleştiriminin nasıl yapıldığından da bahsetmek istiyorum.
std::remove_reference sınıfı partial template specialization ile özelleştirilmiş bir sınıf şablonudur ve isminden de anlaşıldığı üzere amacı argümanın Lvalue referans veya Rvalue referanstan arındırılmış saf türü yakalamaktır.
std::forward
std::forward koşulsal değer kategorisi dönüşümü işlemi yapmaktadır. Argüman olarak verilen Lvalue değer kategorisindeki nesneleri Lvalue olarak Rvalue değer kategorisindeki nesneleri Rvalue olarak geri döndürmektedir. Bu yüzden de mükemmel gönderim mekanizmasında fonksiyon şablonu aracılığı ile argüman olarak geçilen parametreleri değer kategorisinde hiçbir değişikliğe uğratmadan başka bir fonksiyona doğrudan gönderebilmemize yardımcı olmaktadır.
Standard kütüphane gerçekleştiriminde ek olarak static_assert kontrolleri olsa da ana hatlarıyla std::forward gerçekleştirimi yaklaşık olarak yukarıdaki gibidir.
Yukarıdaki kodu analiz edersek görüldüğü üzere std::forward fonksiyon şablonumuz iki farklı overload’u bulunmaktadır.
- İlk overload T’nin türüne bağlı olarak Lvalue bir argümanı Lvalue veya Rvalue olarak değer kategorisi dönüşümünü yapmaktadır.
- İkinci overload ise Rvalue argümanların Lvalue olarak dönüştürülmesine engel olarak Rvalue olarak korunmasına imkan vermektedir.
Bu iki overload’un bunu nasıl yaptığını daha iyi anlamak için önceden bahsettiğim tabloyu kullanarak her senaryo için ayrı ayrı deneme yapabilirsiniz.
Teorik olarak std::move yerine std::forward kullanabilmemiz mümkünken programın mantığı ve okunabilirliği açısından en iyi pratik olarak taşıma semantiği kullanacağımız yerlerde std::move, mükemmel gönderim kullanacağımız yerlerde std::forward kullanmaktayız. Ayrıca std::move kullanırken sadece fonksiyon argümanını vermemiz yeterli iken std::forward kullanımında fonksiyon argümanına ek olarak şablon tür argümanını da vermemiz gerekmektedir.
std::move ve std::forward fonksiyon şablonlarını kullandığımız gösterim için yazdığım örneği aşağıda bulabilirsiniz.
Bu konunun eğer anlaşılmadığını düşünüyorsanız büyük ihtimalle öncelikle Value Category (Değer Kategorisi) konusuyla ilgili bilgilerinizi tazelemeniz gerekmektedir. Value Category konusu bence C++’da en önemli konulardan bir tanesidir.
Diğer yazılarımda görüşmek üzere…
Referanslar