C++ Dilinde auto&& ve Template Type Deduction
- Yusuf Hançar
- Jul 12, 2024
- 8 min read
C++ dilinde tür çıkarımı(type deduction), özellikle modern C++ ile birlikte büyük önem kazanmıştır. Bu özellikler, developer arkadaşlara daha esnek ve güçlü araçlar sunarak kodun daha okunabilir ve bakımının kolay olmasını sağlar. "auto&&" ve şablon tür çıkarımı bu araçların başında gelir. "C++11" standardı ile birlikte gelen auto, derleyicinin değişken türünü otomatik olarak belirlemesine olanak tanır. Bu, kod yazımını basitleştirir ve türe duyarlı hataları azaltır. auto&& ise universal references konseptini tanıtır. Bir fonksiyon şablonunda ya da auto ile birlikte kullanıldığında, evrensel referanslar hem l-value hem de r-value referansları kabul edebilir. "C++14" standartları ile auto ve decltype(auto) kullanımı genişletildi ve geliştirildi. Bu iyileştirmeler, tür çıkarımını daha esnek hale getirir ve daha karmaşık türlerin çıkarılmasını mümkün kılar. "C++17" structured bindings ve decltype(auto) kullanımını daha da genişleterek tür çıkarımına esneklik kazandırır. Bu özellikler, auto&& ve şablon tür çıkarımının daha güçlü ve kullanışlı olmasını sağlar. "C++20" ile birlikte gelen konseptler (concepts), şablon tür çıkarımını daha güvenli ve anlaşılır hale getirir. Konseptler, tür çıkarımının belirli kısıtlamalar altında yapılmasını sağlar, böylece şablonların yanlış kullanımı engellenir. Şimdi örneklerle bu konuları pekiştirmeye çalışalım.
template <typename T>
void func(T, T){}
int main()
{
func(1, 2); // int, int
func(1, 2.); // int, double
}
******************************
CEVAP : syntax error because of ambiguity
******************************
AÇIKLAMA :
template argüman çıkarımı sırasında türlerin uyuşmazlığı ve belirsizlik nedeniyle derleme hatası oluşur.
******************************
template <typename T>
void func(T, T){}
int main()
{
func("smart", "code");
}
******************************
CEVAP : legal
******************************
AÇIKLAMA : " const char* " türü çıkarılır, bu nedenle geçerlidir.
******************************
#include <iostream>
#include <typeinfo>
#include <cxxabi.h>
template <typename T>
void pr_type()
{
int stat;
// gcc ve clang derleyicileri icin mangle edilmistir
char* demangled = abi::__cxa_demangle(typeid(T).name(), 0, 0, &stat);
std::cout << demangled << "\n";
free(demangled);
}
template <typename T>
void func(T, T)
{
std::cout << "1. parameter type : ";
pr_type<T>();
std::cout << "2. parameter type : ";
pr_type<T>();
}
int main()
{
func("smart", "code");
return 0;
}
******************************
CEVAP :
1. parameter type : char const*
2. parameter type : char const*
******************************
AÇIKLAMA :
string literallerinin türleri (const char[6] ve const char[5]) otomatik olarak const char* türüne dönüştürüldü
******************************
template <typename T>
void func(T&, T&){}
int main()
{
func("smart", "code");
}
******************************
CEVAP : syntax error
******************************
AÇIKLAMA : pointer değil(array decay değil) const char[6] ve const char[5] olacaktır.
String literal türleri const char[6] ve const char[5] olarak çıkarılır ve referans olarak alınamadigi icin derleme hatası olusur.
-> func("smart", "smart") seklinde olsaydi gecerli olurdu.
******************************
#include <vector>
#include <string>
template <typename T>
void insert(std::vector<T>& vec, T&& par)
{
vec.push_back(std::forward<T>(par));
}
int main()
{
std::vector<std::string> svec;
std::string str;
insert(svec, str);
}
******************************
CEVAP : syntax error
******************************
AÇIKLAMA : ilk parametrede T türü string olacak, ikinci parametre universal reference parametre yani string& olur ve tür uyusmazligi olur.
******************************
template <typename T>
void pr_type()
{
int stat;
char* demangled = abi::__cxa_demangle(typeid(T).name(), 0, 0, &stat);
std::cout << demangled << "\n";
free(demangled);
}
template <typename T>
void insert(std::vector<std::remove_reference_t<T>>& vec, T&& par)
{
std::cout << "Type T: ";
pr_type<T>();
std::cout << "Type of par: ";
pr_type<decltype(par)>();
vec.push_back(std::forward<T>(par));
}
int main()
{
std::vector<std::string> svec;
std::string str;
insert(svec, str);
return 0;
}
******************************
CEVAP :
Type T: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
Type par: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
******************************
AÇIKLAMA :
******************************
#include <vector>
#include <string>
template <typename First, typename Second>
void insert(std::vector<First>& vec, Second&& par)
{
vec.push_back(std::forward<First>(par));
}
int main()
{
std::vector<std::string> svec;
std::string str;
insert(svec, str);
}
******************************
CEVAP : legal
******************************
AÇIKLAMA :
******************************
template <typename Container, typename T>
void insert(Container& con, T&& par)
{
con.push_back(std::forward<T>(par));
}
int main()
{
std::vector<std::string> svec;
std::string str;
insert(svec, str);
}
******************************
CEVAP : legal
******************************
AÇIKLAMA :
******************************
Artık tür çıkarımında auto ve decltype yaygın olarak kullanılmaktadır. Şimdi auto&& kullanımı ve tür çıkarımı ile ilişkisine değineceğiz. Universal referanslar ile kullanımı, perfomans optimizasyonu ve taşıma semantiği (move semantics)
class Data{};
int main()
{
Data d;
const Data cd;
auto&& r1 = Data{};
/* auto karşılığı yapılan çıkarım Data& ancak reference-collapsing ile Data& e bağlandı. */
auto&& r2 = d;
auto&& r3 = std::move(d);
auto&& r4 = cd;
auto&& r5 = std::move(cd);
}
******************************
CEVAP : legal
******************************
AÇIKLAMA : template argument deduction ile auto deduction bir istisna dışında aynı özelliklerdedir
******************************
class Data{};
template <typename T>
void foo(T){}
int main()
{
Data d;
const Data cd;
foo(d); // foo<Data>d seklinde cikarim yapilir
auto val = d; // val türü Data olacaktir
}
******************************
CEVAP : legal
******************************
auto&& "universal reference" parametre gibi forwarding amaçlı kullanılabilir.
class Data {};
void foo(const Data&)
{
cout << "foo(const Data&)" << endl;
}
void foo(Data&)
{
cout << "foo(Data&)" << endl;
}
void foo(Data&&)
{
cout << "foo(Data&&)" << endl;
}
void foo(const Data&&)
{
cout << "foo(const Data&&)" << endl;
}
int main ()
{
Data d;
const Data cd;
// arguman olarak kullanılacak ifadeye universal reference bağlandı
auto&& r1 = Data{};
// daha sonra doğrudan universal ref. kullanılarak foo'ya çağrı yapıldı.
foo(std::forward<decltype(r1)>(r1));
auto&& r2 = d;
foo(std::forward<decltype(r2)>(r2));
auto&& r3 = cd;
foo(std::forward<decltype(r3)>(r3));
auto&& r4 = std::move(d);
foo(std::forward<decltype(r4)>(r4));
auto&& r5 = std::move(cd);
foo(std::forward<decltype(r5)>(r5));
}
******************************
AÇIKLAMA :
auto&& ifadesi, universal reference olarak adlandırılır. Bu, Data{} bir geçici (temporary) nesne olduğundan, r1 bir rvalue reference (Data&&) olacaktır.
std::forward<decltype(r1)>(r1), türü koruyarak (Data&&), doğru foo fonksiyonunu çağırır: foo(Data&&)
d bir lvalue olduğundan, r2 bir lvalue reference (Data&) olacaktır.
std::forward<decltype(r2)>(r2), türü koruyarak (Data&), doğru foo fonksiyonunu çağırır: foo(Data&).
cd bir const lvalue olduğundan, r3 bir const lvalue reference (const Data&) olacaktır.
std::forward<decltype(r3)>(r3), türü koruyarak (const Data&), doğru foo fonksiyonunu çağırır: foo(const Data&).
std::move(d), d'yi bir rvalue'ya dönüştürür, böylece r4 bir rvalue reference (Data&&) olur.
std::forward<decltype(r4)>(r4), türü koruyarak (Data&&), doğru foo fonksiyonunu çağırır: foo(Data&&).
std::move(cd), cd'yi bir rvalue'ya dönüştürür, böylece r5 bir const rvalue reference (const Data&&) olur.
std::forward<decltype(r5)>(r5), türü koruyarak (const Data&&), doğru foo fonksiyonunu çağırır:
foo(const Data&&).
******************************
int main ()
{
auto&& r = Data{};
foo(std::forward<decltype(r)>(r));
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
foo(Data{});
}
******************************
AÇIKLAMA :
Evrensel Referans ve std::forward ifadeleri gecici birer Data nesnesi olusturur(Data{}).
İlk ifadede auto&& r = Data{} kullanilarak bir evrensel referans olusturulur ve Data&& olarak belirlenir.
std::forward<decltype(r)>(r) kullanilarak, r'in türü korunarak foo fonksiyonuna iletilir.
Bu, geçici nesnenin rvalue reference olarak foo fonksiyonuna iletilmesini saglar. İkinci ifadede ise geçici nesne dogrudan foo fonksiyonuna iletir.
******************************
int main ()
{
Data d;
auto&& r = d;
foo(std::forward<decltype(r)>(r));
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
foo(d);
}
******************************
AÇIKLAMA : Onceki ornekte oldugu gibi cikarimlari yapabiliriz.
******************************
int main ()
{
Data d1;
Data d2;
auto&& r = std::move(d1);
foo(std::forward<decltype(r)>(r));
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
foo(std::move(d2));
}
******************************
AÇIKLAMA : Onceki ornekte oldugu gibi cikarimlari yapabiliriz.
******************************
int main ()
{
const Data cd;
auto&& r = cd;
foo(std::forward<decltype(r)>(r));
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
foo(cd);
}
******************************
AÇIKLAMA : Onceki ornekte oldugu gibi cikarimlari yapabiliriz.
******************************
Peki yukarıdaki işlemler ne işe yarar ?
Fonksiyonun parametre değişkenini doğrudan forward etmek yerine; örneğin bir fonksiyona çağrı yapıp onun geri dönüş değerini bir değişkende tutup ordan elde edilen değeri tekrar göndermek gerektiğinde gerekir.
Buna return value perfect passing denir.
class Data {};
void foo(const Data&)
{
cout << "foo(const Data&)" << endl;
}
void foo(Data&)
{
cout << "foo(Data&)" << endl;
}
void foo(Data&&)
{
cout << "foo(Data&&)" << endl;
}
const Data& func_const_lref(const Data& str)
{
return str;
}
Data& func_non_const_lref(Data& str)
{
return str;
}
Data&& func_rref(Data&& str)
{
return std::move(str);
}
Data func_value(const Data& str)
{
return str;
}
int main ()
{
Data d;
const Data cd;
foo(func_rref(Data{}));
foo(func_non_const_lref(d));
foo(func_const_lref(cd));
foo(func_value(d));
}
******************************
CEVAP :
******************************
AÇIKLAMA :
******************************
int main ()
{
auto&& rf = func_rref(Data{});
foo(std::forward<decltype(rf)>(rf));
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
foo(func_rref(Data{}));
}
T bar(T&& tval)
{
// codes...
return std::forward<T>(tval);
}
teplate <typename T>
void func(T&& tval)
{
foo(bar(std::forward<T>(tval)));
}
******************************
CEVAP :
******************************
AÇIKLAMA : bar fonksiyonuna argumani perfect forward ettik.
bar fonksiyonunun return degerini de foo fonksiyonuna gonderdik. Ancak her zaman bunu yapma durumu yoktur. Bunun nedeni; fonksiyonların argümanlarinin turleri, omurleri veya yan etkileri gibi faktorlere bagli olarak degisebilir.
******************************
T bar(T&& tval)
{
// codes...
return std::forward<T>(tval);
}
teplate <typename T>
void func(T&& tval)
{
auto&& ret = bar(std::forward<T>(tval));
foo(std::forward<decltype(ret)>(ret));
}
******************************
CEVAP :
******************************
AÇIKLAMA : auto&& faydalarından biridir.
generic bir kodda cagirilan fonksiyonun return degerini dogrudan baska fonksiyona gonderildiginde dogal olarak perfect passing yapılır. Bu islem bir degiskende tutup sonra perfect passing işlemini değişkenle yapmak istenilirse auto&& kullanıma mecbur kalinir.
******************************
auto&& faydalarını ve proxy türlerle kullanımına değinelim.
int main()
{
vector<int> ivec(5);
for (auto val : ivec)
{
cout << val;
}
for (auto val : ivec)
{
val = 1;
}
cout << "\n";
for (auto val : ivec)
{
cout << val;
}
}
******************************
CEVAP :
0000
0000
******************************
AÇIKLAMA : değişmez.
******************************
std::vector<bool> ???
int main()
{
vector<bool> ivec(5);
for (auto val : ivec)
{
cout << val;
}
for (auto val : ivec)
{
val = true;
}
cout << "\n";
for (auto val : ivec)
{
cout << val;
}
}
******************************
CEVAP :
0000
1111
******************************
AÇIKLAMA : burada degerler degisir.
std::vector<bool> partial specialization'ı(allocator parametresi oldugu için partial)
vector<bool> partial specialization'ı olusturmadaki amac; boolean degerleri dinamik olarak bitlerde tutmaktir.
******************************
Bu kod parçasında std::vector<bool> kullanıldı ve özel bir durumdur. Çünkü vector<bool> bir bit sıkıştırması uygular ve proxy objeler döndürür. Bu durum, vector<bool> ile çalışırken evrensel referansların önemini vurgular.
int main()
{
vector<bool> ivec(5);
// val'in türü "vector<bool>::reference" yani val türü bool degildir.
auto val = ivec[2];
// auto val = ivec.operator[](2);
}
******************************
CEVAP : legal
******************************
AÇIKLAMA : burada bite reference olmayacağı için proxy object döndürülür.
******************************
int main()
{
vector<bool> ivec(5);
ivec[3] = true;
// yukarıdaki ifade ile asagidaki cagri ayni anlamdadir.
ivec.operator[](3).operator=(true);
}
Derleyici range based for loop için aşağıdaki gibi bir kod üretir.
int main()
{
vector<bool> ivec(5);
for (auto val : ivec)
{
val = true;
}
/*
// ivec L value ya da R value olması farketmez, rng universal reference
auto&& rng = ivec;
auto pos = rng.begin();
auto end = rng.end();
for (; pos != end; ++pos)
{
auto tmp = *pos;
tmp = true;
}
*/
}
int main()
{
vector<bool> ivec(5);
for (auto& val : ivec)
{
}
/*
auto&& rng = ivec;
auto pos = rng.begin();
auto end = rng.end();
for (; pos != end; ++pos)
{
auto& tmp = *pos;
}
*/
}
int main()
{
vector<bool> ivec(4);
for (auto&& val : ivec)
{
}
/*
auto&& rng = ivec;
auto pos = rng.begin();
auto end = rng.end();
for (; pos != end; ++pos)
{
auto&& tmp = *pos;
}
*/
}
Şimdi auto&& nasıl fayda sağlıyor görelim...
template <typename C, typename T>
void fill_container(C& con, const T& val)
{
for (auto& par : con)
{
par = val;
}
}
int main()
{
vector<string> svec{ "first", "sec", "third" };
for (auto val : svec)
{
cout << val << " ";
}
cout << "\n";
fill_container(svec, "fourth");
for (auto val : svec)
{
cout << val << " ";
}
}
******************************
CEVAP :
first sec third
fourth fourth fourth
******************************
template <typename C, typename T>
void fill_container(C& con, const T& val)
{
for (auto& par : con)
{
par = val;
}
}
int main()
{
vector<bool> bvec{ false, false };
for (auto val : bvec)
{
cout << val << " ";
}
cout << "\n";
fill_container(bvec, true);
for (auto val : bvec)
{
cout << val << " ";
}
}
******************************
CEVAP : syntax error (A non-const reference may only be bound to an lvalue)
******************************
AÇIKLAMA : sorun fill_container template kodundaki auto&.
-> üretilen kod şöyle oluyordu.
/*
auto&& rng = con;
auto pos = rng.begin();
auto end = rng.end();
for (; pos != end; ++pos)
{
auto& elem = *pos;
// *pos geri dönüş değeri reference değil çağırılan pos.operator*() ve bununda geri dönüş değeri reference değil class type ve ifade de R value ifadedir.
// yani hatanın nedeni L value reference değişkenine(auto& elem) R value expression bağlamadır.
}
*/
******************************
template <typename C, typename T>
void fill_container(C& con, const T& val)
{
for (auto&& par : con)
{
par = val;
}
}
int main()
{
vector<bool> bvec{ false, false };
for (auto val : bvec)
{
cout << val << " ";
}
cout << "\n";
fill_container(bvec, true);
for (auto val : bvec)
{
cout << val << " ";
}
}
******************************
CEVAP :
0 0
1 1
******************************
AÇIKLAMA : auto&& yaptık yani universal reference ancak burda perfect forwarding amaçlı kullanmadık!. Ayrıca bu vector<bool> için geçerlidir.
******************************
Comentarios