top of page

C++ Dilinde Universal Reference ve Perfect Forwarding

  • Writer: Yusuf Hançar
    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:


  1. const T& şablonunda argümanın değer kategorisi kaybolur ve argümanın rvalue ya da lvalue olduğu fonksiyon içerisinde bilinemez.

  2. 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.
******************************

ree

 
 
 

Recent Posts

See All
C++ Dilinde [[nodiscard]] attribute

C++’ta [[attribute]] (öznitelik) mekanizması, kod hakkında ek bilgi sağlayarak derleyicinin uyarılar vermesine, belirli optimizasyonlar...

 
 
 
C++ Dilinde constinit

C++20'de tanıtılan önemli bir özellik `constinit`'dir; bu özellik, statik depolama süresine sahip değişkenlerin derleme zamanında...

 
 
 

Comments


bottom of page