C++ Dilinde Sağ Taraf Referansları-III
Bu yazımızda sağ taraf referanslarıyla ilgili incelemelerimize devam ediyoruz. Bu bölümde daha önce kullanımlarına örnek verdiğimiz…
C++ Dilinde Sağ Taraf Referansları-III
Bu yazımızda sağ taraf referanslarıyla ilgili incelemelerimize devam ediyoruz. Bu bölümde daha önce kullanımlarına örnek verdiğimiz std::move ve std::forward dönüşüm şablonlarına daha yakından bakacağız.
std::move
Önceki blog yazılarımızda bir isme sahip sağ taraf referanslarının, sağ taraf değerlerine bağlanmalarına karşın, kendilerinin birer sol taraf değeri olduğunu söylemiştik. Bu yüzden sağ taraf referanslarını başka fonksiyonlara argüman olarak geçirirken bir tür dönüşümü yapmak gerekiyordu. Daha önce incelediğimiz bir örneği yeniden hatırlayalım.
#include <iostream>
using namespace std;
class X {};
void bar(X&) {cout << __PRETTY_FUNCTION__ << endl;}
void bar(X&&) {cout << __PRETTY_FUNCTION__ << endl;}
void foo(X&& param) {bar(param);}
int main()
{
foo(X{});
}
Kodu derleyip çalıştıralım.
void bar(X&)
Bir sağ taraf değeri olan X{} için bar(X&&) fonksiyonunun çağrılmasını istememize karşın bar(X&) fonksiyonu çağrılmaktadır. foo fonksiyonunun parametresi olan X&& param bir sol taraf değeri göstermektedir. Bu durumda foo fonksiyonunun parametresi üzerinde tür dönüşümü yaparak bir sağ taraf değeri olarak ele alınmasını sağlayabiliriz. foo fonksiyonunu aşağıdaki gibi yeniden yazabiliriz.
void foo(X&& param) {bar(static_cast<X&&>(param));}
foo fonksiyonunda yukarıdaki değişikliği yaptıktan sonra X{} için bar(X&&) fonksiyonu çağrılacaktır.
Tür dönüşümü yaparak sol taraf değeri gösteren bir ifadenin sağ taraf değeri olarak ele alınmasını sağlayabiliyoruz. Aslında std::move şablonu da tam olarak aynı işlevi görüyor. Aynı işlem std::move ile aşağıdaki gibi yapılmaktadır.
void foo(X&& param) {bar(std::move(param));}
Her seferinde static_cast ile bir tür dönüşümü yapmak yerine bu tür dönüşümünün bir fonksiyon üzerinden yapılması hedeflenmiştir. Özellikle örneğimizdeki X türü yerine uzun bir isme sahip bir tür ile çalışılması durumunda her seferinde türün ismiyle bir dönüşüm yapmak yerine std::move kullanmak hem okunabilirliği arttırmakta hem de yazım kolaylığı sağlamaktadır.
Bu dönüşüm fonksiyonuna move isminin verilmiş olması maalesef kafa karışıklığına neden olabilmektedir. Konuyla ilgili birçok kaynakta bu isimlendirmenin yanlış olduğuyla ilgili (misnomer) yorumlar bulunmaktadır. Aslında std::move aldığı argüman üzerinde herhangi bir taşıma işlemi yapmamakta sadece bir tür dönüşümü yaparak aldığı argümanı taşıma işlemi için aday yapmaktadır. std::move dönüşüm şablonu utility başlık dosyasında tanımlanmıştır. Ayrıca algorithm başlık dosyasında aynı isimli, içsel olarak std::move dönüşüm fonksiyonunu kullanan, ve taşıma işlemi yapan başka bir şablon daha bulunmaktadır. Aşağıdaki bağlantılardan her iki şablonunun da arayüz ve kullanımına bakabilirsiniz.
[embed][std::move 1) Moves the elements in the range [first, last), to another range beginning at d_first, starting from first and…en.cppreference.com](https://en.cppreference.com/w/cpp/algorithm/move)
Biz utility başlık dosyasındaki dönüşüm işlemi için kullanılan std::move ile ilgilenmekteyiz. Şimdi std::move şablonunun nasıl yazıldığına bakalım.
GNU Standard C++ kütüphanesinde std::move şablonu aşağıdaki gibi tanımlanmıştır.
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
return static_cast<typename std::remove_reference<_Tp>::type&&> (__t);
}
Bu aşamada şablondaki constexpr ve noexcept belirleyicileriyle (specifiers) ilgilenmiyoruz. constexpr şablona C++14 ile eklenmiştir, noexcept kullanımına ise daha sonra bakacağız. std::move şablonunu tam olarak anlayabilmek için birkaç noktayı bilmemiz gerekiyor.
İlk olarak şablonlardaki typename kullanımıyla ilgili kuralı hatırlayalım. typename anahtar sözcüğünün class anahtar sözcüğüyle eş değer olan kullanımının dışında bir kullanımı daha mevcuttur. Bir şablon içerisinde şablon tür parametresine bağlı bir ismin (template dependent name) bir tür olarak ele alınabilmesi için typename anahtar sözcüğü kullanılmalıdır. Bu noktada bir örnek üzerinden bu duruma bakalım.
#include <iostream>
template <class T>
void foo(T param)
{
T::type t;
}
int main()
{
return 0;
}
Kodu derlemek isteğimizde aşağıdaki hatayı almaktayız.
error: need ‘typename’ before ‘T:: type’ because ‘T’ is a dependent scope
T::type t;
Derleyici açısından T::type ifadesindeki type isminin bir tür ismimi yoksa T türüne ait static bir değişken mi olduğu açık değildir. typename anahtar sözcüğü kullanılarak type isminin T türünün içsel bir türü olduğu derleyiciye bildirilmiş olur. typename anahtar sözcüğünü eklediğimizde kod derlenmektedir. Elbette foo şablonundan type içsel türüne sahip olmayan türler için kod üretilmeyecektir.
#include <iostream>
template <class T>
void foo(T param)
{
typename T::type t;
}
int main()
{
return 0;
}
std::move şablonunun gövdesindeki ve geri dönüş türündeki typename anahtar sözcüğü bu amaçla kullanılmaktadır.
std::move şablonu içinde şu an için tek bilmediğimiz bölüm remove_reference şablonu şablonuna aittir. remove_reference şablonu referans türü olan bir türün referans türü değilmiş gibi ele alınmasını sağlar. Örnek bir tanımı aşağıdaki gibidir.
template< class T > struct remove_reference {typedef T type;};
template< class T > struct remove_reference<T&> {typedef T type;};
template< class T > struct remove_reference<T&&> {typedef T type;};
Bir türün referans türü olup olmamasına göre farklı remove_reference şablonu ilklendirilecek ve o türe ait içsel bir typedef bildirimi yapılacaktır. Örneğin U, U& ve U&& türleri için aşağıdaki 3 kullanımda da type içsel türü U türüne typedef edilmiştir.
remove_reference<U>::type // U türüne eşittir
remove_reference<U&>::type // U türüne eşittir
remove_reference<U&&>::type // U türüne eşittir
Şimdi std::move şablonunun analizine devam edebiliriz. std::move şablonunun parametresi bir forwarding referanstır. std::move şablonuna argüman olarak U türünden sol veya taraf değerinin geçirildiğini farz edelim. Bu durumda, şablon tür parametresi, T için U& veya U çıkarımları (type deduction) yapılacaktır. std::move şablonunun remove_reference kullanmaksızın sadece static_cast yapacak şekilde aşağıdaki gibi yazıldığını düşünelim.
template<typename _Tp>
_Tp move(_Tp&& __t)
{ return static_cast<_Tp&&>(__t); }
Bu durumda U türünden bir sol taraf değeri için şablon aşağıdaki koda karşılık gelecektir. T türü için U& çıkarımı yapılacaktır.
U& move(U& && __t)
{ return static_cast<U& &&>(__t); }
Referans daraltma sonrasında ise std::move fonksiyonu aşağıdaki gibi olacaktır.
U& move(U& __t)
{ return static_cast<U&>(__t); }
Bu durumda std::move fonksiyonuna geçirilen argüman yine bir sol taraf değeri olarak ele alınacatır. Sol taraf referansı dönen fonksiyonların bir sol taraf değeri oluşturduğunu hatırlayınız. Bu sebeple dönüşüm yapılacak tür bilgisi oluşturulurken remove_reference şablonu kullanılmıştır. remove_reference kullanıldığında yine U türünden bir sol taraf değeri için std::move şablonunun nasıl açıldığına bakalım.
typename std::remove_reference<U&>::type&&
move(U& && __t)
{
return static_cast<typename std::remove_reference<U&>::type&&>(__t); }
remove_reference şablonu açıldığında ve referans daraltım kuralı uygulandığında std::move fonksiyonu aşağıdaki gibi olacaktır.
U&& move(U& __t)
{ return static_cast<U&&>(__t); }
Bu sayede U türünden bir sol tarafı, U&& türüne tür dönüşümü yapılarak, bir sağ taraf değeri olarak ele alınabilir. std::move şablonuna geçirilen argümanın bir sağ taraf değeri olması durumunda ise std::move fonksiyonu aşağıdaki gibi olacaktır.
U&& move(U&& __t)
{ return static_cast<U&&>(__t); }
std::move ile dikkat edilmesi gereken bir diğer konu ise, daha önce de söylediğimiz gibi, std::move işleminin kendisine geçirilen argümanı taşıma işlemi için aday yapmasıdır. std::move taşıma işlemini garanti etmemektedir. Bir örnek üzerinden bu durumu inceleyelim.
#include <iostream>
using namespace std;
class X
{
public:
X() {}
X(const X&) {cout << __PRETTY_FUNCTION__ << endl;}
};
int main()
{
X x1;
X x2 = std::move(x1);
return 0;
}
Kode derleyip çalıştıralım.
X::X(const X&)
X türü taşıma işlemini desteklemediği yani move constructor fonksiyonuna sahip olmadığı için x2 nesnesi taşıma değil kopyalama yoluyla oluşturulmaktadır. Bildiğimiz üzere std::move bir sağ taraf değeri üretmekte ve bu sağ taraf değeri const bir sol taraf referansına bağlanabilmektedir.
Bir nesnenin const olması durumuna ise dikkat edilmelidir. Bir nesnenin const olması durumunda da std::move kullanılarak taşıma işlemi yapılamamaktadır.
#include <iostream>
class X
{
public:
X() {}
X(const X&) {std::cout << __PRETTY_FUNCTION__ << std::endl;}
X(X&&) {std::cout << __PRETTY_FUNCTION__ << std::endl;}
};
int main()
{
const X x1;
X x2 = std::move(x1);
}
Kodu derleyip çalıştıralım.
X::X(const X&)
std::move, const bir sağ taraf değeri üretecek ve derleyici tarafından aday constructor fonksiyonlarından const parametreli olan yani kopyalayan constructor fonksiyonu seçilecektir.
Şimdi forwarding referanslarla beraber kullanılan std::forward şablonuna bakalım.
std::forward
std::forward, ilklendirildiği türe bağlı olarak sol veya sağ taraf değeri üretmek için kullanılır. Daha önce incelediğimiz mükemmel gönderim (perfect forwarding) örneğini hatırlayalım.
#include <iostream>
using namespace std;
class X {};
void bar(X&) {cout << __PRETTY_FUNCTION__ << endl;}
void bar(X&&) {cout << __PRETTY_FUNCTION__ << endl;}
template <typename T>
void foo(T&& param) {bar(std::forward<T>(param));}
int main()
{
X x;
foo(x);
foo(X{});
}
İlk olarak aşağıdaki gibi std::forward kullanılmaksızın foo şablonunun sol ve sağ taraf değerleri için ilklendirildiğini düşünelim.
#include <iostream>
using namespace std;
class X {};
void bar(X&) {cout << __PRETTY_FUNCTION__ << endl;}
void bar(X&&) {cout << __PRETTY_FUNCTION__ << endl;}
template <typename T>
void foo(T&& param) {bar(param);}
int main()
{
X x;
foo(x);
foo(X{});
}
Derleyici tür belirleme ve referans daraltma kurallarını uygulayarak foo(x) ve foo(X{}) için aşağıdaki eşdeğer foo fonksiyonlarını üretecektir.
void foo(X& param) {bar(param);}
void foo(X&& param) {bar(param);}
Her iki fonksiyonda da param bir sol taraf değeridir ve nihayetinde bar(X&) fonksiyonu çağrılacaktır. foo fonksiyonu bir sağ taraf değeri olan X{} için oluşturulmuş ise bu durumda bar(X&&) fonksiyonu çağrılmalıdır. forwarding referansların açıldığı ifadenin değer kategorisine göre farklı ele alındığını hatırlayınız. U türünden bir sol taraf değeri için U& çıkarımı yapılırken sağ taraf değeri için U çıkarımı yapılmaktadır. Aslında ifadenin değer kategorisi çıkarım yapılan tür içerisinde kodlanmış durumdadır. Bu durumda yine std::move örneğinde olduğu gibi static_cast ile bir tür dönüşümü yapmak mümkündür. Örneğimizi bu şekilde değiştirelim.
#include <iostream>
using namespace std;
class X {};
void bar(X&) {cout << __PRETTY_FUNCTION__ << endl;}
void bar(X&&) {cout << __PRETTY_FUNCTION__ << endl;}
template <typename T>
void foo(T&& param) {bar(static_cast<T&&>(param));}
int main()
{
X x;
foo(x);
foo(X{});
}
Kodu derleyip çalıştırdığımızda beklediğimiz gibi çalıştığını görüyoruz.
void bar(X&)
void bar(X&&)
U türünden sol ve sağ taraf değerleri için foo şablonunun nasıl ilklendirildiğine bakalım. foo şablonuna geçirilen argüman bir sol taraf ifadesi ise foo şablonu aşağıdaki gibi ilklendirilecektir.
foo(U& && param) {bar(static_cast<U& &&>(param));}
//referans daraltım kuralı uygulanırsa
foo(U& param) {bar(static_cast<U&>(param));}
Zaten bir sol taraf değeri olan param için U& referans türüne yapılan dönüşümün bir etkisi olmayacak ve bar(U&) fonksiyonu çağrılacaktır. foo şablonuna geçirilen argümanın bir sağ taraf değer olması durumunda ise şablon aşağıdaki gibi açılacaktır.
foo şablonuna geçirilen argümanın bir sağ taraf değer olması durumunda ise şablon aşağıdaki gibi açılacaktır.
foo(U && param) {bar(static_cast<U&&>(param));
Bir sol taraf değeri olan param, U&& türüne tür dönüşümü sonucu bir sağ taraf değeri olarak ele alınacak ve nihayetinde bar(U&&) fonksiyonu çağrılacaktır. std::forward şablonu da std::move şablonu gibi yine bir tür dönüşümü yapmaktadır. Fakat yine bu işlem, static_cast kullanılarak, açık bir tür dönüşümü yapılmak yerine bir fonksiyon üzerinden yapılmak istendiğinde işler bir miktar karışmaktadır.
std::forward şablonu GNU Standard C++ kütüphanesinde aşağıdaki gibi tanımlanmıştır.
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
std::forward şablonunda ilk bakışta anlaşılması en zor noktayı fonksiyon parametresi oluşturmaktadır. Fonksiyon şablonunun parametresi, std::move şablonunun aksine, forwarding referans olacak şekilde yazılmamıştır. İlk olarak bu kullanımım gerekçesini anlamaya çalışalım.
Bir fonksiyon şablonun hangi tür veya türler için açılacağı derleyici tarafından bilinmelidir. Şablonun hangi türler için açılacağı ya açık bir şekilde belirtilmeli ya da derleyicinin belirlemesine bırakılmalıdır. Derleyici fonksiyon çağrısındaki argüman veya argümanların türlerine bakarak şablonun hangi tür veya türler için açılacağını belirleyebilir (template argument deduction). Aşağıdaki örnek üzerinden bu kullanımlara bakalım.
#include <iostream>
using namespace std;
template <typename T>
void foo(T param) {cout << __PRETTY_FUNCTION__ << endl;}
int main()
{
foo(0); // (1)
foo<double>(0); // (2)
}
Kodu derleyip çalıştıralım.
void foo(T) [with T = int]
void foo(T) [with T = double]
1. kullanımda şablonun hangi tür için açılacağı, fonksiyona geçirilen argümanın türü göz önüne alınarak, derleyici tarafından belirlenmiştir. 2. kullanımda ise şablonun hangi tür için ilklendirileceği açık bir şekilde belirtilmiş ve derleyicinin tür belirlemesine izin verilmemiştir. foo şablonunun her iki şekilde de ilklendirilebildiğini görüyoruz. Peki bir sebepten derleyicinin tür çıkarımı yaparak şablonu açmasını, yani 1. kullanımı, engellemek isteseydik ne yapabilirdik?
#include <iostream>
using namespace std;
template<typename T>
struct identity { typedef T type; };
template <typename T>
void foo(typename identity<T>::type param)
{
cout << __PRETTY_FUNCTION__ << endl;
}
int main()
{
foo(0);
}
Kodu derlemeye çalıştığımızda aşağıdaki hatayı almaktayız.
error: no matching function for call to ‘foo(int)’
identity şablonu hangi tür için ilklendirilmişse içsel type türü ilklendirildiği türe karşılık gelmektedir. Yani identity<T>::type ile T türü eşdeğerdir. identity şablonu argüman üzerinden tür belirlemesini engellemek için bu şekilde kullanılabilmektedir. Bu durumda derleyici T türününün açık olarak geçirilmesini beklemektedir.
std::forward şablonundaki remove_reference kullanımı da benzer şekilde derleyicinin tür belirlemesi yaparak şablonu ilklendirmesini engellemek içindir. Yani std::forward şablonunun hangi tür için açılacağının açık bir şekilde belirtilmesi gerekmektedir. Şimdi örnek bir std::forward kullanımına tekrar bakarak bu durumu anlamaya çalışalım.
template <typename T>
void foo(T&& param) {bar(std::forward<T>(param));}
foo şablonu hangi değer kategorisine ait ifade için ilklendirilmiş olursa olsun param daima bir sol tarafı göstermektedir. Bu durumda std::forward şablonu kendisine argüman olarak geçirilen param üzerinden tür çıkarımı yapılarak ilklendirilecek olursa daima bir sol taraf değeri üzerinden ilklendirilecektir. Fakat std::forward şablonunun duruma göre sol veya sağ taraf değeri üretmesi istenmektedir. foo fonksiyonunun hangi değer kategorisi için açıldığı T şablon parametresinden anlaşılmaktadır. Bu yüzden bu tür bilgisi açık bir şekilde std::forward şablonuna geçirilmelidir.
Aşağıdaki örnek kullanım üzeriden sol ve sağ taraf değerleri için std::forward şablonunun nasıl ilklendirildiğine bakalım. Örneğimizde U herhangi bir türü temsil etmektedir.
template <typename T>
void foo(T&& param) {bar(std::forward<T>(param));}
int main()
{
U u;
foo(u);
}
foo şablonunun parametresi forwarding referans olduğundan dolayı sol taraf değeri için, referans daraltma sonrasında, foo aşağıdaki şekle dönüşecektir.
foo(U& param) {bar(std::forward<U&>(param));}
std::forward ise U& için aşağıdaki gibi ilklendirilecektir.
U& && forward(typename std::remove_reference<U&>::type& __t)
{ return static_cast<U& &&>(__t); }
// remove_reference açılırsa
U& && forward(U& __t)
{ return static_cast<U& &&>(__t); }
//referans daraltım sonrasında
U& forward(U& __t)
{ return static_cast<U&>(__t); }
Bir sol taraf değeri için std::forward yine bir sol taraf değeri üretmektedir. Şimdi bir sağ taraf değeri için şablonunun nasıl ilklendirildiğine bakalım.
template <typename T>
void foo(T&& param) {bar(std::forward<T>(param));}
int main()
{
foo(U{});
}
foo şablonu aşağıdaki gibi ilklendirilecektir.
foo(U && param) {bar(std::forward<U>(param));}
std::forward ise U için aşağıdaki gibi açılacaktır.
U && forward(typename std::remove_reference<U>::type& __t)
{ return static_cast<U&&>(__t); }
// remove_reference açılırsa
U&& forward(U& __t)
{ return static_cast<U&&>(__t); }
Yukarıdaki ifadelerden görüldüğü gibi sağ taraf değeri için std::forward yine bir sağ taraf değeri üretmektedir.
Bir sonraki yazımızda buraya kadar incelediğimiz konuları toparlayarak fonksiyonlara argümanların nasıl geçirilebileceğini inceleyeceğiz.
메타데이터
- post_id
- ddef30a03c48
- slug
- c-dilinde-sağ-taraf-referansları-iii-ddef30a03c48
- url
- https://medium.com/@serkaneser13/c-dilinde-sa%C4%9F-taraf-referanslar%C4%B1-iii-ddef30a03c48
- canonical_url
- https://medium.com/@serkaneser13/c-dilinde-sa%C4%9F-taraf-referanslar%C4%B1-iii-ddef30a03c48
- author_url
- https://medium.com/@serkaneser13
- status
- ok
- fetched_at
- 2026-07-21 11:03:45