Gönderen: Necati Ergin C++, C++11, mükemmel gönderim, perfect forwarding, R value references, sağ taraf referansı
mükemmel gönderim (perfect forwarding)
Sağ taraf referanslarının çözüm sağladığı ikinci problem “mükemmel gönderim”. Önce bir örnekle problemin ne olduğunu anlamaya çalışalım. Aşağıdaki gibi bir fabrika işlevimiz olsun:
template<typename T, typename Arg> std::shared_ptr<T> factory(Arg arg) { return std::shared_ptr<T>(new T(arg)); }
1
2
3
4
5
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg arg)
{
return std::shared_ptr<T>(new T(arg));
}
Koddan da anlaşıldığı gibi burada amaç factory işlevine gönderilen argüman olan ifadenin yani arg‘nin factory işlevi tarafından T sınıfının kurucu işlevine gönderilmesini sağlamak. Yukarıdaki kod kesinikle bu işi yapmayacak, neden? Bir kere arg ifadesinin bu şablondan üretilecek factory işlevine aktarımında bir kopyalama söz konusu olacak. Oysa arg ifadesinin T sınıfının kurucu işlevine referans yoluyla aktarılması gerekiyorsa, yani kurucu işlevin parametresi referans ise, bu yöntem her şeyden önce fazladan bir kopyalamaya neden olacak.
Bu durumda akla gelen ilk çözüm, kurucu işleve çağrı yapacak olan factory işlevinin parametresinin de referans olması:
template<typename T, typename Arg> std::shared_ptr<T> factory(Arg& arg) { return std::shared_ptr<T>(new T(arg)); }
1
2
3
4
5
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg& arg)
{
return std::shared_ptr<T>(new T(arg));
}
Bu şüphesiz öncekine göre daha iyi ama mükemmel değil. Çünkü şimdi de başka bir sorunumuz var: factory işlevinin bir sağ tarafı değeri ifadesi ile çağrılması mümkün değil.
factory<X>(foo()); // foo işlevi değerle dönüyorsa geçersiz factory<X>(41); // geçersiz
1
2
factory<X>(foo()); // foo işlevi değerle dönüyorsa geçersiz
factory<X>(41); // geçersiz
Bu probleme de bir çözüm sağlamak için bu kez argümanı const referans ile alacak ikinci bir yükleme (overload) yazılabilir:
template<typename T, typename Arg> std::shared_ptr<T> factory(Arg const &arg) { return std::shared_ptr<T>(new T(arg)); }
1
2
3
4
5
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg const &arg)
{
return std::shared_ptr<T>(new T(arg));
}
Bu yaklaşımda da iki ayrı sorun var. Birinci sorun şu: factory işlevinin birden fazla parametre değişkeni var ise, const olan ya da olmayan tüm parametre değişkeni kombinasyonları için ayrı yüklemeler yapmamız gerekecek. Yani parametre değişkeni sayısı arttıkça yazılması geren işlev sayısı da üstel şekilde artacak.
İkinci sorun, bu şekilde gönderimin taşıma semantiğini bloke etmesi. factory işlevinin gövdesinde kurucu işleve gönderilen ifade bir sol taraf değeri. Bu yüzden sarmalayıcı bir işlev olmadan taşıma semantiğinin devreye girme şansı yok.
Bu sorunun çözümü için sağ taraf referansları kullanılabilir. Sağ taraf referanslarının kullanılmasıyla ilave bir yükleme yapılmadan mükemmel gönderim sağlanabilir. Bunun nasıl gerçekleştirilebileceğini anlayabilmek için sağ taraf referanslarına ilişkin bazı kuralları bilmemiz gerekiyor:
İnceleyeceğimiz kurallardan birincisi sol taraf referanslarını da ilgilendiriyor. C++11 öncesinde, bir referansa referans almak (reference to reference) mümkün değildi.
Yani örneğin A& & gibi bir türün oluşturulma girişimi sentaks hatasına neden olurdu. Buna karşılık C++11, aşağıdaki referans bozunum kurallarını (reference collapsing) getiriyor:
A& & türü A& A& && türü A& A&& & türü A& A&& && türü A&& olarak ele alınır.
1
2
3
4
A& & türü A&
A& && türü A&
A&& & türü A&
A&& && türü A&& olarak ele alınır.
İkinci kural da şu: Eğer bir şablon işlevin işlev parametresi bir sağ taraf referansı ise şablon tür parametresinin ve buna bağlı olarak işlev parametresinin çıkarımı farklı bir şekilde yapılır. Aşağıdaki koda bakalım:
template<typename T> void foo(T&&);
1
2
template<typename T>
void foo(T&&);
Yukarıdaki gibi bir şablonun T && biçiminde tanımlanan parametresine popüler olarak “universal reference” deniyor. Bu Scott Meyers tarafından uydurulan bir terim. Kullanılan bir başka terim de forwarding references (aktarım referansı).
A bir tür olmak üzere foo işlevi A türünden bir sol taraf değeri ile çağrılırsa T türünün çıkarımı A& türü olarak yapılır.
Bu durumda yukarıda açıklanan bozunun kurallarına göre işlevin parametresi A& türünden olur.
foo işlevi bir sağ taraf değeri ile çağrıldığında ise, T türünün çıkarımı A türü olarak yapılır. Bu durumda işlevin parametre değişkeni A && türünden olur. Bu kuralları anladıktan sonra artık çözüme girişebiliriz:
template<typename T, typename Arg> std::shared_ptr<T> factory(Arg&& arg) { return std::shared_ptr<T>(new T(std::forward<Arg>(arg))); }
1
2
3
4
5
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg)
{
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
forward işlev şablonu da şu şekilde tanımlanabilir:
template<class S> S&& forward(typename remove_reference<S>::type& a) noexcept { return static_cast<S&&>(a); }
1
2
3
4
5
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
İşlev şablonunda yer alan noexcept anahtar sözcüğüne şimdilik takılmayalım. Bu anahtar sözcük, derleyicinin bazı optimizasyon işlemlerini yapabilmesi için işlevimizin bir hata nesnesi göndermeyeceğini belirtiyor. Bu noktaya daha sonra geri döneceğiz.
Yukarıdaki kodun mükemmel gönderimi nasıl sağladığını incelemeye başlayabiliriz. Bu amaçla factory işlevinin hem bir sol taraf değeri ile hem de bir sağ taraf değeri ile çağrılması durumlarını ayrı ayrı ele alacağız.
A ve X türler olsun. Önce factory<A> işlevinin X türünden bir sol taraf değeri ile çağrıldığını düşünelim:
X x; factory<A>(x);
1
2
X x;
factory<A>(x);
Daha önce belirttiğimiz çıkarım kurallarına göre, bu durumda derleyici şablon tür parametresi olan Arg türünün çıkarımını X& türü olarak gerçekleştirecek. Bu durumda derleyici factory ve forward şablonlarından aşağıdaki şekilde işlevler üretecek:
std::shared_ptr<A> factory(X& && arg) { return std::shared_ptr<A>(new A(std::forward<X&>(arg))); } X& && forward(remove_reference<X&>::type& a) noexcept { return static_cast<X& &&>(a); }
1
2
3
4
5
6
7
8
9
std::shared_ptr<A> factory(X& && arg)
{
return std::shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& && forward(remove_reference<X&>::type& a) noexcept
{
return static_cast<X& &&>(a);
}
remove_reference değerlendirildikten ve refererans bozunum kuralları uygulandıktan sonra kod şu hale dönüşecek:
std::shared_ptr<A> factory(X& arg) { return strd::shared_ptr<A>(new A(forward<X&>(arg))); } X& std::forward(X& a) { return static_cast<X&>(a); }
1
2
3
4
5
6
7
8
9
std::shared_ptr<A> factory(X& arg)
{
return strd::shared_ptr<A>(new A(forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
Bu sol taraf değerleri için kesinlikle bir mükemmel gönderim. factory işlevine gönderilen arg ifadesi A sınıfının kurucu işlevine iki kademede sol taraf referansı olarak geçiliyor.
Şimdi de, factory<A> işlevinin X türünden bir sağ taraf değeri ile çağrıldığını düşünelim:
X foo(); factory<A>(foo());
1
2
X foo();
factory<A>(foo());
Yine çıkarım kurallarına göre factory şablonunun şablon tür parametresi olan Arg türünün çıkarımı X türü olarak gerçekleştirilecek.
Bu durumda derleyici iki şablon için aşağıdaki kodları yazmış olacak:
std::shared_ptr<A> factory(X&& arg) { return std::shared_ptr<A>(new A(std::forward<X>(arg))); } X&& forward(X& a) noexcept { return static_cast<X&&>(a); }
1
2
3
4
5
6
7
8
9
std::shared_ptr<A> factory(X&& arg)
{
return std::shared_ptr<A>(new A(std::forward<X>(arg)));
}
X&& forward(X& a) noexcept
{
return static_cast<X&&>(a);
}
Bu da sağ taraf değerleri içim mükemmel bir gönderim. factory işlevine geçilen argüman olan arg her iki kademede A sınıfının kurucu işlevine referans semantiği ile gönderiliyor. Üstelik, A sınıfının kurucu işlevi kendisine geçilen argümanı bir ismi olmadığı için bir sağ taraf değeri olarak ele alacak. Birinci yazımızda anlatılan isimsizlik kuralına göre ifade sağ taraf değeri olarak ele alınıyor. Bu durumda A sınıfının kurucu işlevi bir sağ taraf değeri ile çağrılmış oluyor. Yani taşıma semantiği (move semantics) korunmuş oluyor. Sanki factory sarmalayıcısı hiç yokmuş gibi gönderilen argüman özellikleri korunarak kurucu işleve gönderilmiş oluyor.
std::forward işlev şablonunun burada kullanılmasının tek nedeninin taşıma semantiğinin korunması olduğunu söyleyebiliriz. std::forward işlevi kullanılmasaydı her şey yine aynı şekilde çalışırdı. Ancak A sınıfının kurucu işlevi kendisine gönderilen argümanı, bir ismi olacağından bir sağ taraf değeri olarak değil bir sol taraf değeri olarak ele alırdı. Başka bir deyişle, std::forward işlevinin amacı aslında, çağrı noktasıda ifadenin sol taraf değeri mi sağ taraf değeri mi olduğu bilgisini iletmek.
Peki neden std::forward işlevinde remove_reference kullanımına gereksinim duyuluyor? Aslında ihtiyaç duyulmuyor. forward şablonunun tanımında
remove_reference<S>::type&
1
remove_reference<S>::type&
yerine yalnızca
S&
1
S&
yazılsaydı, hem sol taraf değeri hem de sağ taraf değeri için ne olacağını ayrı ayrı siz de adım adım takip edip kendiniz anlayabilirsiniz. Gönderim kesinlikle yine aynı şekilde mümkün olacaktı. Ancak bizim bu sonucu elde edebilmemiz için forward şablonunu kullanırken şablon tür argümanını açıkça yazmamız gerekecekti. forward şablonunun tanımında remove_reference kullanılmasının amacı bizi buna, yani şablon tür argümanını açıkça belirtmeye zorlamak.
Şimdi de std::move işlevinin gerçekleştirimine bakalım. std::move işlev şablonunun amacının kendisine gönderilen argümanı referans yoluyla yakalayıp bir sağ taraf değeri olarak kullanılmasını sağlamak:
Gerçekleştirim şu şekilde:
template<class T> typename remove_reference<T>::type&& std::move(T&& a) noexcept { typedef typename remove_reference<T>::type&& RvalRef; return static_cast<RvalRef>(a); }
1
2
3
4
5
6
7
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
move işlevini X sınıfı türünden bir sol taraf değeri ile çağırdığımızı düşünelim:
X x; std::move(x);
1
2
X x;
std::move(x);
Yeni çıkarım kurallarına göre, şablon tür parametresi olan T‘nin çıkarımı X& türü olarak yapılacak. Yani derleyici şöyle bir kod yazmış olacak:
typename remove_reference<X&>::type&& std::move(X& && a) noexcept { typedef typename remove_reference<X&>::type&& RvalRef; return static_cast<RvalRef>(a); }
1
2
3
4
5
6
typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
typedef typename remove_reference<X&>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
remove_reference ele alındıktan ve yeni referans bozunum kuralları uygulandıktan sonra kod şu hale gelecek:
X&& std::move(X& a) noexcept { return static_cast<X&&>(a); }
1
2
3
4
X&& std::move(X& a) noexcept
{
return static_cast<X&&>(a);
}
Bu da istediğimiz iş görülüyor demek, değil mi? Sol taraf değeri olan x ifademiz bir sol taraf referansına bağlandı ve işlevimiz onu isimsiz bir sağ taraf değerine dönüştürerek gönderdi.
move işlevinin sağ taraf değeri olan ifadeler için çağrıldığında da doğru çalışacağını anlamayı size bırakıyoruz. Ancak move‘un tek amacı bir sol taraf değerini bir sağ taraf değerine dönüştürmek ise neden move bir sağ taraf değeri ile çağrılsın ki diye sorabilir ve isterseniz bunu pas geçebilirsiniz.
Şunu da fark ettiniz mi?
std::move(x)
1
std::move(x)
yerine
static_cast<X&&>(x)
1
static_cast<X&&>(x)
ifadesini de yazabilirdik.
Ancak ne yapıldığını daha iyi ifade ettiği için kesinlikle move tercih edilmeli.
Bu yazı Thomas Becker‘in “C++ Rvalue References Explained” isimli makalesinin serbest çevirisidir.
Yazı dolaşımı
C çalışma soruları – 51 (strstr işlevi)
taban sınıf olarak tasarlanmamış sınıflardan türetme yapmaktan kaçının
Necati Ergin
C ve Sistem Programcıları Derneğinde eğitmen olarak çalışıyor.
Bunlar da ilginizi çekebilir
yüklenmiş işlev çözümlemesinde özel durumlar – 4 (const yüklemesi)
16 Aralık 2016 Necati Ergin yüklenmiş işlev çözümlemesinde özel durumlar – 4 (const yüklemesi) için yorumlar kapalı
delege eden kurucu işlev (delegating constructor)
26 Nisan 2017 Necati Ergin delege eden kurucu işlev (delegating constructor) için yorumlar kapalı
X makrosu
29 Kasım 2016 Necati Ergin X makrosu için yorumlar kapalı
Kod Eklemek İçin Okuyun
Eklemek istediğiniz kodları lütfen aşağıdaki “pre” kodları arasında yazınız.

<pre class="lang:c++ decode:true ">

--yazacağınız kodlar--

</pre>
(buradan kopyalayarak kullanabilirsiniz)
Plepa Eğitim Hizmetleri
Plepa Eğitim Hizmetleri, başta C ve C++ programlama dilleri olmak üzere, programlama ve yazılım geliştirme konusunda eğitim ve danışmanlık hizmetleri vermektedir.
Bizi Takip Edin
İletişim
Düzenlediğimiz eğitim programları hakkında bilgi edinmek ya da eğitim taleplerinizi iletmek için info@plepa.com adresine e-posta mesajı gönderebilirsiniz.