C++ Dilinde Universal Reference ve Perfect Forwarding
- Yusuf Hançar
- Jun 22, 2024
- 7 min read
Updated: Jun 28, 2024
Daha önceki yazılarımızda reference semantiği ve taşıma semantiği üzerine kodlar ve bunlarla ilgili alt başlıklara değindik. Özellikle modern C++11 ile gelen R Value reference taşıma semantiği ve perfect forwarding ile iç içe ve en önemli konulardandır. Bu konuya başlarken "&&" ifadesi görüldüğünde R value reference diyerek önüne arkasını görmeden atılmak eksik bilgi ve tehlikeli yollara götürebilir diyerek uyarımızı yapalım...! Temel olarak "r value" ve "type declaration" kavramları yakın cevaplar olarak verilse de yavaş yavaş bol örneklerle bu ayrımı oturtmaya çalışacağız.
C++ programlama dilinde artık kaç çeşit reference var denildiğinde; L value, R value ve konumuz olan forwarding reference(Scott Meyers ile gelen yaygın isim Universal reference) yani 3 cevabını veriyoruz.
Universal reference, özel bir syntax ile oluşturulur ve her türden const ya da non-const nesne buna bağlanabilir. Aşağıda vereceğimiz temel sentaksta lütfen sağ taraf reference ile karıştırılmaması uyarısına tekrar dikkat edelim...
template <typename T>
void unismartcode(T&&);
template <typenane T>
void csmartcode(const T&);
******************************
AÇIKLAMA : buna da üstteki gibi(universal reference) her türlü argüman gönderilebilir. R value expression sol taraf referansına bağlanır. L value expression const sol taraf referansına bağlanır.
******************************
Her iki şablona da çeşitli argüman türleri gönderilebilir. Ancak, "const T&" olan fonksiyona gönderilen varlığın değer kategorisi kaybedilir. Yani, gelen argümanın rvalue ya da lvalue olup olmadığı fonksiyon içerisinde bilinemez. Ayrıca, "const" özelliği de kaybolur. Diğer yandan, T&& olan şablonda bu bilgiler korunur. Universal reference (evrensel referans) sayesinde bu bilgiler derleme zamanında kullanılabilir. Universal referanslar, fonksiyona geçirilen argümanın rvalue ya da lvalue olduğunu ve "const" olup olmadığını tutar ve bu bilgiyi korur.
Bu farklar aşağıdaki gibi özetlenebilir:
const T& şablonunda argümanın değer kategorisi kaybolur ve argümanın rvalue ya da lvalue olduğu fonksiyon içerisinde bilinemez.
T&& şablonu ise bu bilgileri korur ve derleme zamanında kullanılmasına olanak sağlar.
class Data{};
template <typename T>
void unismartcode(T&& arg){}
int main()
{
Data d;
unismartcode(d);
}
******************************
AÇIKLAMA : T türü Data& olunca bu durumda sağ taraf referansına sol taraf referansı atanır ve reference collapsing kurallarına göre sol taraf referansına bağlanır.
T -> Data&
arg -> Data&
******************************
CEVAP :
******************************
int main()
{
unismartcode(Data{});
}
******************************
AÇIKLAMA : T -> Data
arg -> Data&&
******************************
int main()
{
const Data d;
unismartcode(d);
}
******************************
AÇIKLAMA : T -> const Data&
arg -> const Data&
******************************
CEVAP :
******************************
std::forward
#include <iostream>
#include <utility>
class Data {};
void func(const Data& arg)
{
std::cout << "L-value reference func called" << std::endl;
}
void func(Data&& arg)
{
std::cout << "R-value reference func called" << std::endl;
}
template <typename T>
void unismartcode(T&& arg)
{
// eğer T türü referans türü ise
func(arg)
// eğer T türü referans türü değil ise
func(std::move(arg))
}
******************************
AÇIKLAMA : bu şekilde yazmak yerine c++ ın sunduğu contexpr dönüşüm fonksiyonu olan std::forward kullanılabilir. sol taraf değeri ise sol taraf değeri olarak döndürür. sağ taraf değeri ise isim L value ifade olmasına rağmen sağ taraf referansına dönüştürür ve bunu yaparken T türünü ya da arg yi kullanarakta yapabilir.
******************************
template <typename T>
void unismartcode(T&& arg)
{
// C++'ın sağladığı constexpr dönüşüm fonksiyonu olan std::forward ile kullanım.
bar(std::forward<T>(arg));
}
int main()
{
Data d;
// L-value referansı olarak çağrı
unismartcode(d);
// R-value referansı olarak çağrı
unismartcode(Data());
}
******************************
AÇIKLAMA : perfect forwarding
******************************
Modern C++'ta perfect forwarding, bir fonksiyona argümanları doğrudan başka bir fonksiyona iletirken orijinal değer kategorilerini (lvalue veya rvalue) koruma yeteneği sağlar. Bu özellik, performansı artırmak ve gereksiz kopyalamalardan kaçınmak için kritik öneme sahiptir.
1. Emplace Fonksiyonları
Konteyner sınıflarının emplace_back gibi fonksiyonları, perfect forwarding ile argüman alır. Bu, argümanların doğrudan konteyner elemanlarının oluşturulmasında kullanılmasını sağlar ve gereksiz kopyalamalardan kaçınılır.
2. push_back Fonksiyonu
Konteyner sınıflarının push_back fonksiyonu, hem const referans (const &) hem de rvalue referans (&&) overload'larına sahiptir. Ancak, emplace_back fonksiyonu perfect forwarding kullanarak daha verimli bir şekilde çalışır.
3. make_unique Fonksiyonu
make_unique fonksiyonu, bir unique_ptr nesnesi oluşturur ve onu döndürür. Bu işlem sırasında dinamik ömürlü bir nesne oluşturmak için new ifadesini kullanır. Dinamik ömürlü nesnenin constructor'ına argümanları perfect forwarding ile geçirir.
#include <vector>
#include <string>
#include <iostream>
class Data {
public:
Data(const std::string& name) : m_name(name)
{
std::cout << "l-value constructor\n";
}
Data(std::string&& name) : m_name(std::move(name))
{
std::cout << "r-value constructor\n";
}
private:
std::string m_name;
};
int main()
{
std::vector<Data> dval;
std::string name = "Smart";
dval.push_back(name); // Calls l-value constructor
dval.push_back(std::move(name)); // Calls r-value constructor
widgets.emplace_back("Code"); // Calls r-value constructor
return 0;
}
******************************
AÇIKLAMA :
******************************
#include <memory>
#include <iostream>
class Data {
public:
Data(int a, double b)
{
std::cout << "Data constructed with values: " << a << " and " << b << "\n";
}
};
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main()
{
auto sc = std::make_unique<Data>(5, 3.14);
return 0;
}
class Data {};
void func(const Data&)
{
cout << "const Data&" << "\n";
}
void func(Data&)
{
cout << "Data&" << "\n";
}
void func(Data&&)
{
cout << "Data&&" << "\n";
}
void func(const Data&&)
{
cout << "const Data&&" << "\n";
}
int main()
{
Data d;
const Data cd;
func(d); // Data&
func(cd); // const Data&
func(Data{}); // Data&&
func(std::move(d)); // Data&&
func(std::move(cd)); // const Data&&
}
void call_func(const Data& d)
{
func(d);
}
void call_func(Data& d)
{
func(d);
}
void call_func(Data&& d)
{
func(std::move(d));
}
void call_func(const Data&& d)
{
func(std::move(d));
}
int main()
{
Data d;
const Data cd;
call_func(d); // Data&
call_func(cd); // const Data&
call_func(Data{}); // Data&&
call_func(std::move(d)); // Data&&
call_func(std::move(cd)); // const Data&&
}
template <typename T>
void call_func(T&& arg)
{
func(std::forward<T>(arg));
}
int main()
{
Data d;
const Data cd;
call_func(d); // Data&
call_func(cd); // const Data&
call_func(Data{}); // Data&&
call_func(std::move(d)); // Data&&
call_func(std::move(cd)); // const Data&&
}
Forward kullanımı en basit haliyle aşağıdaki gibidir.
template <class T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept
{
return static_cast<T&&>(t);
}
******************************
AÇIKLAMA : C++20 ve sonrası typename kullanmadan da yazabiliriz.
******************************
template <typename T>
void call_func(T&& arg)
{
func(std::forward<T>(arg));
}
int main()
{
Data d;
const Data cd;
auto fn = [](auto&& r) { func(std::forward<decltype(r)>(r)); };
fn(d); // Data&
fn(cd); // const Data&
fn(Data{}); // Data&&
fn(std::move(d)); // Data&&
fn(std::move(cd)); // const Data&&
}
#include <iostream>
class Data {
public :
Data() = default;
Data(const Data&)
{
cout << "const Data&" << "\n";
}
Data(Data&&)
{
cout << "Data&&" << "\n";
}
template <typename T>
Data(T&&)
{
cout << "universal reference" << "\n";
}
};
int main ()
{
Data val;
const Data cval;
Data a{ cval }; // copy ctor
Data b{ val }; // universal reference
Data c{ std::move(val) }; // move ctor
Data d{ std::move(cval) }; // universal reference
}
Universal reference ile ilgili kullanım senaryoları ve temel sentaksı ile ilgili okumalarımız ve örneklendirmelerimizle bu noktaya geldik. Ara ara c++17 sonrasına da dokunmaya çalıştık ki artık 2023 ve 2026 standartları da konuşulmaya geliştirilmeye başladı. Şimdi biraz daha universal reference sentaksı ve bu yapıyı oluşturmadan ara standartların da etkilerine değinelim.
#include <uitiliy>
template <typename T>
void foo(T&& val)
{
std::forward<T>(val);
}
Yukarıdaki sentaks başlarda değindiğimiz temel sentaks ancak bazı noktalarda dilin sunduğu özellikler ile farklı kullanımlara kaçabiliriz. Bunlardan biri template parametresi olan genel olarak "T" olarak gördüğümüz parametreyi kullanma durumu söz konusu değilse decltype specifier kullanarak sentaksta oynama yapabiliriz.
template <typename T>
void foo(T&& val)
{
std::forward<decltype(val)>(val);
}
Lambda ifadelerindeki kullanımlarda template parametresi olmadığı senaryolar olacaktır ve bu durumlarda decltype ile kullanılabilir.
int main()
{
auto fn = [](auto&& val){ std::forward<decltype(val)>(val) };
}
#include <iostream>
#include <utility>
int main()
{
auto fn = [](auto&& val){ return std::forward<decltype(val)>(val); };
int sc{10};
int& ref = sc;
// fn L-value reference ile cagirildi.
auto retval = fn(ref);
// retval hala bir L-value referansı olacaktır
std::cout << "l-value reference : " << retval << "\n";
return 0;
}
#include <iostream>
#include <utility>
int main()
{
auto fn = [](auto&& val){ return std::forward<decltype(val)>(val); };
// fn R-value ile cagirildi
auto retval = fn(10);
// retval bir R-value referrence olur
std::cout << "R-value : " << retval << "\n";
return 0;
}
#include <iostream>
#include <utility>
int main()
{
auto fn = [](auto&& val) { return std::forward<decltype(val)>(val); };
const int cs = 10;
const int& c_ref = cs;
// fn const l-value reference ile cagirildi
auto retval = fn(c_ref);
// retval hala const l-value reference
std::cout << "const l-value reference : " << retval << "\n";
return 0;
}
#include <iostream>
#include <utility>
int main()
{
auto fn = [](auto&& val) { return std::forward<decltype(val)>(val); };
// fn const R-value ile cagirildi
auto retval = fn(std::move(10));
// retval const R-value reference olur
std::cout << "const R-value : " << ret << "\n";
return 0;
}
C++20 ile lambda ifadelerinde template parametreleri kullanılabilir.
int main()
{
auto fn = []<typename T>(T& val) { std::forward<T>(val) };
}
#include <iostream>
#include <utility>
void smartshow(const std::string& str)
{
std::cout << "L-value : " << str << "\n";
}
void smartshow(std::string&& str)
{
std::cout << "R-value : " << str << "\n";
}
int main()
{
auto fn = []<typename T>(T&& sc) { smartshow(std::forward<T>(sc)); };
std::string str = "SmartCode";
fn(str); // with L-value reference
fn(std::move(str)); // with R-value reference
fn("new code"); // with R-value reference
return 0;
}
C++20 standartları ile gelen abbreviated function template sentaksı ile kullanım daha da sadeleştirilebilir. "https://en.cppreference.com/w/cpp/language/function_template"
template <typename T>
void func(T sc);
// alltaki kullanım aynı anlamdadır ve template olarak değerlendirilir.
void func(auto sc);
template <typename T>
void func(T& sc);
// alltaki kullanım aynı anlamdadır ve template olarak değerlendirilir.
void func(auto& sc);
template <typename T>
void func(T&& sc);
// alltaki kullanım aynı anlamdadır ve template olarak değerlendirilir.
void func(auto&& sc);
perfect forwarding veya universal referanslar, sadece değer kategorisini korumak için değil, aynı zamanda const özelliğini korumak için de kullanılabilir. Bu kullanım, bir argümanın const olup olmadığını doğru bir şekilde iletmek ve buna göre davranmak için önemlidir.
#include <iostream>
#include <utility>
class SmartCode {
public:
void modify()
{
std::cout << "Modifiable SmartCode " << "\n";
}
void print() const
{
std::cout << "Const SmartCode" << "\n";
}
};
// parameter T is universal reference
template <typename T>
void process(T&& arg)
{
helper(std::forward<T>(arg));
}
void helper(SmartCode& sc)
{
std::cout << "Non-const SmartCode reference passed to helper." << "\n";
sc.modify();
}
void helper(const SmartCode& sc)
{
std::cout << "Const SmartCode reference passed to helper." << "\n";
sc.print();
}
int main()
{
SmartCode val;
const SmartCode cval;
process(val); // Non-const SmartCode ref passed to helper func. Modifiable SmartCode
process(cval); // Const SmartCode ref passed to helper func. Const SmartCode
return 0;
}
const olma durumunu derleme zamanında kontrol edebiliriz.
#include <type_traits>
#include <iostream>
#include <string>
template <typename T>
void process(T&& val)
{
if constexpr (std::is_const_v<std::remove_reference_t<T>>)
{
std::cout << "const argument"" << "\n";
}
else
{
std::cout << "non const argument"" << "\n";
}
}
int main()
{
std::string str{ "non const str" };
const std::string c_str{ "const str" };
process(str); // non const
process(c_str); // const
}
******************************
CEVAP :
******************************
AÇIKLAMA : constluk korunndu ve bunu compile time da test ettik.
******************************
Değer kategorisini de derleme zamanında kontrol edebiliriz.
#include <type_traits>
#include <iostream>
#include <string>
template <typename T>
void process(T&&)
{
if constexpr (std::is_lvalue_reference_v<T>)
{
std::cout << "L value argument" << "\n";
}
else
{
std::cout << "R value argument" << "\n";
}
}
class SmartCode{};
int main()
{
process(SmartCode{});
SmartCode sc;
process(sc);
process(std::move(sc));
}
******************************
CEVAP :
R value argument
L value argument
R value argument
******************************
AÇIKLAMA : template'e gelen T parametresi; L value ifade ile cagirilirsa sol taraf reference type, R value expression ile cagirilirsa non-reference type olur ve constluk korunmaya devam eder.
******************************

Comments