top of page

C++ Dilinde Mandatory 'Copy Elision' ve Temporary Materialization

  • Writer: Yusuf Hançar
    Yusuf Hançar
  • May 9, 2024
  • 6 min read

Updated: May 21, 2024

C++ dilinde geliştirme yaparken ihtiyacımız ve dilin sağladığı araçlara göre kopyalama veya taşıma işlemleri gerçekleştirmekteyiz ya da derleyici bunu optimize etmektedir. Performans ve optimizasyonun sağlanabilmesi için özellikle gereksiz durumlarda kopyalamadan kaçınmak her zaman faydamıza olacaktır. Bu yazımızda kopyalama süreçlerinin optimizasyonu ve eliminasyonu ile C++17 itibari ile değişen ve geliştirilen kurallara değineceğiz.

Öncelikle copy elision dediğimiz kopyalamadan kaçınma diye ifade edilebilir. En önemli şart bir nesne oluşturma sürecinin olmasıdır.


  • Aşağıdaki durumlarda copy elision söz konusu olmaktadır.

  1. In the initialization of an object

  2. In a return statement

  3. In a throw expression

  4. In a catch clause


#include <iostream>

class SmartCode {
public:
    SmartCode() 
    {
        std::cout << "SmartCode ctor called" << "\n";
    }

    SmartCode(const SmartCode&) 
    {
        std::cout << "SmartCode copy ctor called" << "\n";
    }

    ~SmartCode() 
    {
        std::cout << "SmartCode dtor called" << "\n";
    }
};

int main() 
{
    SmartCode sc;

    try 
    {
        throw sc; 
    } 
    catch(const SmartCode&) 
    {
        std::cout << "exception caught\n"; 
    }

    return 0;
}
*****************************
AÇIKLAMA : throw ifadesi tarafından oluşturulan exception object, sc nesnesinin kendisi değil, derleyicinin copy elision optimizasyonu kullanarak doğrudan oluşturulmuş bir nesnedir. Bu durum, kopyalama maliyetinden kaçınılmasını ve kodun daha verimli hale gelmesini sağlar.
*****************************
CEVAP : 
SmartCode ctor called
SmartCode copy ctor called
exception caught
SmartCode dtor called
SmartCode dtor called
*****************************

class SmartCode {
public :
    SmartCode()
    {
        cout << "def ctor" << endl;
    }

    SmartCode(int)
    {
        cout << "SmartCode(int)" << endl;
    }

    SmartCode(const SmartCode&)
    {
        cout << "copy ctor" << endl;
    }

    SmartCode(SmartCode&&)
    {
        cout << "move ctor" << endl;
    }

    SmartCode& operator=(const SmartCode&)
    {
        cout << "copy assignment" << endl;
        return *this;
    }

    SmartCode& operator=(SmartCode&&)
    {
        cout << "move assignment" << endl;
        return *this;
    }
};

int main()
{
    SmartCode sc;
    SmartCode sc1(5);
    SmartCode sc2(sc1);
    SmartCode sc3(std::move(sc1));
}
*****************************
AÇIKLAMA : 
*****************************
CEVAP : 
def ctor
SmartCode(int)
copy ctor
move ctor
*****************************

  • Değer kategorilerinden ve senaryolarından bahsetmiştik. C++17 standartları ile "PR value" kategorisi kavramı değişerek doğrudan bir nesne kendisi değil ilk değer vermeye yönelik bir kavram haline gelmiştir. Bir nesne oluşturulma zorunluluğu olduğu durumda "PR value" ifade "X value" ifadeye dönüştürülerek temporary materialization süreci işletir. Oluşturulan bu neneye de result object denir.


int main()
{
	SmartCode sc = SmartCode{ SmartCode{ SmartCode{} } };
}
*****************************
AÇIKLAMA : eski kurallara göre yorumlansaydı. SmartCode{} için def ctor, diğer ikisi içinde copy ctor çağırılırdı.Ancak burada mandatory copy elision vardır. PR value object söz konusu ise result object bulunana kadar herhangi bir nesne oluşturulmaz.
*****************************
CEVAP : def ctor
*****************************

class SmartCode {
public :
    SmartCode()
    {
        cout << "def ctor" << endl;
    }

    SmartCode(int)
    {
        cout << "SmartCode(int)" << endl;
    }

    SmartCode(const SmartCode&) = delete;
    SmartCode(SmartCode&&) = delete;

    SmartCode& operator=(const SmartCode&)
    {
        cout << "copy assignment" << endl;
        return *this;
    }

    SmartCode& operator=(SmartCode&&)
    {
        cout << "move assignment" << endl;
        return *this;
    }
};

int main()
{
    SmartCode sc = SmartCode{ SmartCode{ SmartCode{} } };
}
*****************************
AÇIKLAMA : C++17 öncesi olsaydı sentaks hatası olurdu. Ancak copy elision gerçekleştiği için kopyalama söz konusu değildir ve geçerlidir.
*****************************
CEVAP : def ctor
*****************************

void func(SmartCode)
{
	// ...
}

int main()
{
    func(SmartCode{});
}
*****************************
AÇIKLAMA : SmartCode{} ifadesi bir PR value expression, çağrılan fonksiyonun parametre değişkenini init edecek. mandatory copy elision vardır.
*****************************
CEVAP : def ctor
*****************************

SmartCode func(int sc)
{
	return SmartCode{ sc };
}

int main()
{
    auto val = func(24);
}
*****************************
AÇIKLAMA : 
->Bu optimizasyon, geçici nesnelerin kopyalanmadan doğrudan hedef nesneye taşınmasını sağlar. func(24) çağrısı, bir geçici SmartCode nesnesi oluşturur ve val değişkenine atanır.
->"copy elision" gerçekleştiğinde, normalde geçici nesnenin oluşturulması, ardından bu geçici nesnenin val değişkenine kopyalanması gerekir. Ancak derleyici, bu kopyalama işlemi yerine doğrudan geçici nesneyi val değişkenine oluşturur. Böylece, gereksiz kopyalama işlemleri engellenir ve performans artırılır. "copy ctor" ya da "move ctor" yerine "def ctor" çağırılır.
*****************************
CEVAP : SmartCode(int)
*****************************

void func(const SmartCode&)
{
	// ...    
}

int main()
{
    func(SmartCode{ 24 });
}
*****************************
AÇIKLAMA : func fonksiyonu SmartCode türünden bir referans parametre alır ve func çağrıldığında, SmartCode{24} ifadesi bir geçici SmartCode nesnesini oluşturur ve func fonksiyonuna bir referans olarak geçirilir.

Geçici nesne, bir referansa veya bir const referansa bağlanabilir. Bu durumda, geçici nesne SmartCode türünden bir const referansa bağlanır. Bu işlem, geçici nesnenin somutlaştırılmasını gerektirir, çünkü bir referansın bağlandığı nesnenin bellekte var olması gerekir.

func(SmartCode{24}) çağrısında, SmartCode{24} ifadesi bir geçici nesne oluşturur ve bu geçici nesne doğrudan func fonksiyonuna bir const referans olarak geçirilir. C++ dilinde, derleyiciler bu tür durumlarda gereksiz kopya işlemlerini zorunlu olarak atlayabilir ve geçici nesneyi doğrudan fonksiyon parametresine geçirebilir. Bu nedenle, bu durumda derleyiciler "mandatory copy elision" kuralını uygularlar.

Sonuç olarak, func(SmartCode{24}) çağrısı sırasında, derleyicilerin zorunlu copy elision kullanarak geçici nesneyi doğrudan fonksiyon parametresine aktarması beklenir. Bu, gereksiz kopya işlemlerini engeller ve kodun daha verimli olmasını sağlar.
*****************************
CEVAP : SmartCode(int)
*****************************

class SmartCode {
public :
    SmartCode()
    {
        cout << "def ctor" << endl;
    }

    ~SmartCode()
    {
        cout << "dtor" << endl;
    }

    SmartCode(int)
    {
        cout << "SmartCode(int)" << endl;
    }

    SmartCode(const SmartCode&) = delete;

    SmartCode& operator=(const SmartCode&)
    {
        cout << "copy assignment" << endl;
        return *this;
    }

    SmartCode& operator=(SmartCode&&)
    {
        cout << "move assignment" << endl;
        return *this;
    }
};

SmartCode foo()
{
    SmartCode val;

    return val;         // syntax error
}

int main()
{
    SmartCode mx = foo();
}
*****************************
AÇIKLAMA : Fonksiyonun geri dönüş değerinin bir yere yazılması için kopyalama gerekir ancak sınıfın copy ctor'ı delete edilmiştir. delete edilen fonksiyona çağrı yapmak hatalıdır. 
*****************************
CEVAP : syntax error
*****************************

L value reference değişkenine R value reference ile ilk değer verilmez!!!
int main()
{
    SmartCode& sc = SmartCode{};
}
*****************************
CEVAP : syntax error
*****************************

int main()
{
    const SmartCode& sc = SmartCode{};
}
*****************************
AÇIKLAMA : temporary materialization
copy elision yoktur.
*****************************
CEVAP : def ctor
*****************************

int main()
{
    SmartCode&& sc = SmartCode{};
}
*****************************
CEVAP : def ctor
*****************************

class SmartCode {
public :
	...
    
    int func()
    {
        return 24;
    }
};

int main()
{
    auto sc = SmartCode{}.func(); 
}
*****************************
AÇIKLAMA : temporary materialization
*****************************
CEVAP : def ctor
*****************************

class SmartCode {
public :
	...
};

using ustr_t = std::unique_ptr<string>;

ustr_t func()
{
    auto uptr = std::make_unique<std::string>("smartcode");

    return uptr;
}

int main()
{
    SmartCode sc = func();
}
*****************************
AÇIKLAMA : std::unique_ptr move only class.
L value R value dönüşümü yapılmıştır.
*****************************
CEVAP : def ctor
*****************************

copy elision yapılamayan bazı durumlar...
  • Parametre değişkeni return ediliyorsa copy elision beklenmemelidir.

class SmartCode {
public :
    SmartCode()
    {
        cout << "def ctor" << endl;
    }

    SmartCode(const SmartCode&)
    {
        cout << "copy ctor" << endl;
    }
    
    SmartCode(SmartCode&&)
    {
        cout << "move ctor" << endl;
    }
};

SmartCode func(SmartCode val)
{
    return val;                 
}

int main()
{
    SmartCode sc;
    SmartCode ce{ func(sc) };
}
******************************  
CEVAP :   
def ctor
copy ctor
move ctor
******************************  
AÇIKLAMA : geri dönüş değeri parametre değişkenidir.
parametre değişkenini kendisine gönderilen adreste oluşturma imkanı yoktur. NRVO gerçekleşmeyecektir.
******************************

  • Fonksiyonun return değerinin bir koşula bağlı farklı nesneler olması durumunda yapılmaz. Run-time ile ilgili olduğu için derleyicinin static olarak bilmesi mümkün değildir.

class SmartCode {
public :
    SmartCode()
    {
        cout << "def ctor" << endl;
    }

    SmartCode(int)
    {
        cout << "SmartCode(int)" << endl;
    }

    SmartCode(const SmartCode&)
    {
        cout << "copy ctor" << endl;
    }
    
    SmartCode(SmartCode&&)
    {
        cout << "move ctor" << endl;
    }

    SmartCode& operator=(const SmartCode&)
    {
        cout << "copy assignment" << endl;
        return *this;
    }
    
    SmartCode& operator=(SmartCode&&)
    {
        cout << "move assignment" << endl;
        return *this;
    }
};

void foo(){}
void bar(){}

SmartCode f1(int x)
{
    SmartCode sc{x};

    foo();

    return x > 10 ? sc : sc;
}

SmartCode f2(int x)
{
    auto sc = SmartCode{x};

    if (x > 10)
    {
        return sc;
    }

    return SmartCode{ x + 5 };
}

SmartCode f3(int x)
{
    if (x > 10)
    {
        SmartCode sc{x};
        
        foo();
        
        return sc;
    }
    else
    {
        return SmartCode{ x + 3 };
    }
}

SmartCode f4(int x)
{
    if (x > 10)
    {
        SmartCode sc{x};
        
        foo();
        
        return sc;
    }
    else
    {
        SmartCode sc{ x + 5 };
        
        bar();
        
        return sc;
    }
}

int main()
{
    auto sc = f1(1);
}
******************************  
CEVAP :   
SmartCode(int)
copy ctor
******************************  
AÇIKLAMA : 
f1 fonksiyonu aynı nesneyi döndürmesine rağmen ternary operatoru kullanmıştır. 2. ve 3. ifadeleri L value expression ise 2 farklı ihtimal olduğundan derleyici burada copy elision yapamıyor. Bunun yanı sıra move ctor da çağırılmıyor. Doğrudan copy ctor çağırılır.
******************************

int main()
{
    auto sc = f2(2);
}
******************************  
CEVAP :   
SmartCode(int)
move ctor
******************************  
AÇIKLAMA : 
f2 içinde bir yerel değişken tanımlı ve ona ilk değer verilmiş. PR value expr olduğu için (SmartCode(x)) mandatory copy elision vardır.
Koşul dışında geçici nesne ile değer döndürülüyor ve 2 farklı return ifadesi olsa da copy elision gerçekleştirdi.
******************************

int main()
{
    auto sc = f3(3);
}
******************************  
CEVAP :   
SmartCode(int)
move ctor
******************************  
AÇIKLAMA : 
f3 için scope'larda farklıdır. İlk if ifadesinde sc döndürülüyor ve if bloğu ile sınırlıyken, else kısmında temporary object döndürülüyor.
gcc copy elision yapabiliyor, clang ve microsoft yapamadı.
******************************

 
 
 

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

 
 
 

Comentarios


bottom of page