sağ taraf referansları – 2

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:

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ı:

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.

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:

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:

İ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:

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:

forward işlev şablonu da şu şekilde tanımlanabilir:

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

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:

remove_reference değerlendirildikten ve refererans bozunum kuralları uygulandıktan sonra kod şu hale dönüşecek:

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:

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:

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

yerine yalnızca

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:

move işlevini X sınıfı türünden bir sol taraf değeri ile çağırdığımızı düşünelim:

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:

remove_reference ele alındıktan ve yeni referans bozunum kuralları uygulandıktan sonra kod şu hale gelecek:

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?

yerine

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.

Necati Ergin

C ve Sistem Programcıları Derneğinde eğitmen olarak çalışıyor.

Bunlar da ilginizi çekebilir

sağ taraf referansları – 2” için bir yorum

  1. Hocam, std::return… yazmışsınız return std::… yerine bilginiz olsun. Bir de strd var std yerine 🙂

Bir Cevap Yazın

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

Kod Eklemek İçin Okuyun