← Kursa Dön
📄 Text · 18 min

Copy Elision, RVO, Converting Constructors ve explicit

C++ derleyicileri (compiler) düşündüğünden çok daha zeki. Senin yazdığın kodda gereksiz kopyalama işlemleri varsa, derleyici bunları sessizce atlayabiliyor. Bu optimizasyona copy elision denir. Aynı derste, sınıfların istemeden otomatik tür dönüşümü yapmasını ve bunu explicit keyword'ü ile nasıl kontrol edeceğimizi de öğreneceğiz.

Bu iki konu birbiriyle doğrudan bağlantılı: ikisi de constructor'ların gizli davranışlarıyla ilgili. Haydi başlayalım.


Copy Elision Nedir?

Copy elision, derleyicinin gereksiz kopya (copy) veya taşıma (move) işlemlerini atlaması anlamına gelir. Yani sen bir nesne kopyalanacak diye kod yazarsın ama derleyici "buna gerek yok, doğrudan hedefe oluşturayım" der.

Analoji: Kargo Analojisi 📦

Bir fabrikayı düşün. Ürün üretiliyor, kutuya konuyor, kutu kamyona yükleniyor, kamyon mağazaya gidiyor, kutu raftan alınıyor. Normal akış bu.

Ama akıllı bir lojistik müdürü der ki: "Neden ürünü doğrudan mağazanın rafına üretmiyoruz?" İşte copy elision tam olarak bu. Derleyici, nesneyi geçici yere koymak yerine doğrudan hedef konumda oluşturur. Gereksiz paketleme-boşaltma işlemi yok.


RVO — Return Value Optimization

RVO, copy elision'ın en yaygın formudur. Bir fonksiyon değer döndürdüğünde (by value), normalde şu olur:

  1. Fonksiyon içinde nesne oluşturulur

  2. Nesne, döndürülmek üzere geçici bir yere kopyalanır

  3. Çağıran tarafta başka bir yere kopyalanır

Ama RVO ile derleyici tüm bu kopyalamaları atlar ve nesneyi doğrudan çağıran tarafın belleğinde oluşturur.

Kod Örneği: Copy Constructor'a Log Koyalım

#include <iostream>
#include <string>

class Widget {
private:
    std::string name;

public:
    // Normal constructor
    Widget(const std::string& n) : name(n) {
        std::cout << "Constructor: " << name << std::endl;
    }

    // Copy constructor
    Widget(const Widget& other) : name(other.name + " (kopya)") {
        std::cout << "Copy Constructor: " << name << std::endl;
    }

    // Move constructor
    Widget(Widget&& other) noexcept : name(std::move(other.name)) {
        name += " (tasinmis)";
        std::cout << "Move Constructor: " << name << std::endl;
    }

    ~Widget() {
        std::cout << "Destructor: " << name << std::endl;
    }

    std::string getName() const { return name; }
};

// RVO - isimsiz (unnamed) gecici nesne dondurme
Widget createWidget() {
    return Widget("Fabrikadan");  // Gecici nesne dogrudan donduruluyor
}

int main() {
    std::cout << "=== RVO Testi ===" << std::endl;
    Widget w = createWidget();
    std::cout << "Sonuc: " << w.getName() << std::endl;

    return 0;
}

C++17 ve sonrası çıktı:

=== RVO Testi ===
Constructor: Fabrikadan
Sonuc: Fabrikadan
Destructor: Fabrikadan

Bak ne oldu! Copy constructor hiç çağrılmadı. Move constructor da çağrılmadı. Derleyici, Widget("Fabrikadan") nesnesini doğrudan w'nin bellek adresinde oluşturdu. Sanki Widget w("Fabrikadan") yazmışsın gibi.


NRVO — Named Return Value Optimization

NRVO, RVO'nun bir adım ötesi. RVO'da isimsiz (anonymous/temporary) nesne döndürüyorduk. NRVO'da ise isimli bir lokal değişken döndürüyoruz ve derleyici yine de kopyayı atlayabiliyor.

Widget createNamedWidget() {
    Widget w("Isimli Widget");  // Isimli lokal degisken
    // ... bazi islemler ...
    return w;  // NRVO: w dogrudan cagiran tarafta olusturulabilir
}

int main() {
    std::cout << "=== NRVO Testi ===" << std::endl;
    Widget w2 = createNamedWidget();
    std::cout << "Sonuc: " << w2.getName() << std::endl;

    return 0;
}

Çoğu modern derleyici (GCC, Clang, MSVC) bunu da optimize eder ve copy/move constructor çağırmaz. Ama burada önemli bir fark var...

⚠️ Dikkat: RVO (isimsiz geçici nesne) C++17'den itibaren garantilidir — derleyici bunu yapmak zorundadır. Ama NRVO (isimli değişken) hâlâ isteğe bağlıdır — derleyici yapabilir ama yapmak zorunda değildir. Pratikte modern derleyiciler genellikle NRVO'yu da uygular ama buna güvenme.


C++17: Mandatory Copy Elision (Garantili Kopya Atlama)

C++17 öncesinde copy elision bir optimizasyondu — derleyici yapabilirdi ama yapmak zorunda değildi. Hatta bazı durumlarda copy constructor erişilebilir olmalıydı (silinmiş veya private olamazdı), optimizasyon uygulansa bile.

C++17 ile birlikte belirli durumlarda copy elision zorunlu hale geldi. Yani:

  1. Derleyici kopyayı mutlaka atlar

  2. Copy/move constructor'ın erişilebilir olması bile gerekmez

Hangi Durumlarda Garantili?

class NonCopyable {
public:
    NonCopyable() { std::cout << "Olusturuldu!" << std::endl; }

    // Kopyalama ve tasima YASAK
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = delete;
};

NonCopyable create() {
    return NonCopyable();  // C++17: Tamam! Copy elision garantili.
                           // C++14: HATA! Copy constructor deleted.
}

int main() {
    NonCopyable obj = create();  // C++17: Calisir!
    return 0;
}

C++17'de bu kod derlenip çalışır. Çünkü derleyici NonCopyable() nesnesini doğrudan obj'nin adresinde oluşturur — kopyalama veya taşıma hiç söz konusu olmaz.

Garantili Copy Elision'ın Geçerli Olduğu Durumlar

  1. Geçici nesneden başlatma (initialization from temporary):

``cpp Widget w = Widget("test"); // Garantili: gecici -> w Widget w2 = createWidget(); // Garantili: fonksiyondan gecici doner ``

  1. return ifadesinde geçici nesne:

``cpp Widget create() { return Widget("test"); // Garantili } ``

Garantili Olmayan Durumlar

Widget createNamed() {
    Widget w("test");
    return w;  // NRVO - garantili DEGIL (ama genellikle uygulanir)
}

Widget choose(bool flag) {
    Widget a("A");
    Widget b("B");
    if (flag) return a;
    return b;  // Hangisini dondurecegi belli degil - NRVO uygulanamaz
}

İkinci örnekte derleyici hangi nesneyi döndüreceğini derleme zamanında bilemez, bu yüzden NRVO uygulayamaz. Bu durumda move constructor devreye girer.


Copy Elision'ı Devre Dışı Bırakmak

Test veya öğrenme amacıyla copy elision'ı kapatmak isteyebilirsin:

# GCC/Clang icin
g++ -fno-elide-constructors -std=c++14 main.cpp -o main

# C++17'de bile (-fno-elide-constructors) mandatory elision'i kapatamazsin
# Sadece isteye bagli (non-mandatory) elision'i kapatir

💡 İpucu: Copy elision var diye "kopyalama maliyetli olsa da by-value döndürelim" diye düşünme. NRVO her zaman garanti değil. Büyük nesnelerde yine de dikkatli ol. Ama küçük-orta nesnelerde by-value döndürmek modern C++'ta gayet kabul edilebilir ve clean bir API sağlar.


Converting Constructors — Gizli Tür Dönüşümü

Şimdi tamamen farklı ama çok önemli bir konuya geçiyoruz. C++'ta tek parametreli constructor'lar, fark etmeden otomatik tür dönüşümü yapabilir. Buna "converting constructor" veya "implicit conversion constructor" denir.

Analoji: Otomatik Tercüman 🗣️

Bir toplantıda Türkçe konuşuyorsun. Birisi İngilizce bir cümle söylüyor ama senin yanında oturan tercüman hemen çeviriyor, sen fark bile etmeden. Bazen bu harika — iletişim akıyor. Ama bazen tercüman yanlış çeviriyor ve sen fark etmeden yanlış anlıyorsun. İşte converting constructor da böyle: bazen hayat kurtarır, bazen sessizce bug üretir.

Sorun: İstenmeyen Implicit Dönüşüm

#include <iostream>

class Fraction {
private:
    int numerator;
    int denominator;

public:
    // Tek parametreli constructor - converting constructor!
    Fraction(int n, int d = 1) : numerator(n), denominator(d) {
        std::cout << "Fraction olusturuldu: "
                  << numerator << "/" << denominator << std::endl;
    }

    void print() const {
        std::cout << numerator << "/" << denominator << std::endl;
    }
};

void process(Fraction f) {
    std::cout << "Processing: ";
    f.print();
}

int main() {
    Fraction f1(3, 4);  // Normal: 3/4
    Fraction f2 = 5;    // Surpriz! int -> Fraction donusumu. 5/1 olur.

    process(f1);         // Normal: Fraction gonderiyoruz
    process(5);          // Surpriz! 5 otomatik olarak Fraction(5) olur!
    process(42);         // Bu da calisir! 42 -> Fraction(42, 1)

    return 0;
}

Çıktı:

Fraction olusturuldu: 3/4
Fraction olusturuldu: 5/1
Processing: 3/4
Fraction olusturuldu: 5/1
Processing: 5/1
Fraction olusturuldu: 42/1
Processing: 42/1

process(5) yazdık ve derleyici hiç şikayet etmeden 5'i Fraction(5) olarak dönüştürdü. Bazı durumlarda bu istenen bir davranış olabilir ama çoğu zaman tehlikelidir.

Neden Tehlikeli?

class Distance {
    double meters;
public:
    Distance(double m) : meters(m) {}
    double getMeters() const { return meters; }
};

class Weight {
    double kg;
public:
    Weight(double k) : kg(k) {}
    double getKg() const { return kg; }
};

void calculateForce(Distance d, Weight w) {
    std::cout << "Mesafe: " << d.getMeters() << "m, "
              << "Agirlik: " << w.getKg() << "kg" << std::endl;
}

int main() {
    // Tamamiyla yanlis ama derleniyor!
    calculateForce(75.0, 100.0);  // 75 metre mi? 100 kg mi?
                                   // Yoksa tam tersi mi olmaliydi?

    // Asil niyet bu muydu?
    // calculateForce(Distance(100.0), Weight(75.0));

    return 0;
}

Parametrelerin sırası karışsa bile derleyici ses çıkarmıyor. doubleDistance ve doubleWeight dönüşümü otomatik yapılıyor. Bu ciddi bir hata kaynağı.


explicit Keyword — Kurtarıcı

explicit keyword'ü, bir constructor'ın implicit (örtük) dönüşüm yapmasını engeller. Constructor'ın önüne explicit koyduğunda, artık sadece açıkça (explicitly) çağrılabilir.

Temel Kullanım

#include <iostream>

class Fraction {
private:
    int numerator;
    int denominator;

public:
    // explicit ile artik implicit donusum YOK
    explicit Fraction(int n, int d = 1)
        : numerator(n), denominator(d) {}

    void print() const {
        std::cout << numerator << "/" << denominator << std::endl;
    }
};

void process(Fraction f) {
    std::cout << "Processing: ";
    f.print();
}

int main() {
    Fraction f1(3, 4);           // OK: dogrudan constructor cagrisi
    Fraction f2{5};              // OK: brace initialization
    // Fraction f3 = 5;          // HATA! implicit donusum engellendi
    // process(5);               // HATA! int -> Fraction donusmez

    process(Fraction(5));        // OK: acikca Fraction olusturuyoruz
    process(Fraction{5});        // OK: brace initialization ile
    // process(static_cast<Fraction>(5));  // OK: acik donusum

    return 0;
}

Artık process(5) derlenmez. Derleyici sana "int'ten Fraction'a implicit dönüşüm yapılamaz" der. Bu çok güzel çünkü yanlışlıkla yanlış tür göndermeni engeller.

Distance ve Weight'i Güvenli Hale Getirelim

class Distance {
    double meters;
public:
    explicit Distance(double m) : meters(m) {}
    double getMeters() const { return meters; }
};

class Weight {
    double kg;
public:
    explicit Weight(double k) : kg(k) {}
    double getKg() const { return kg; }
};

void calculateForce(Distance d, Weight w) {
    std::cout << "Mesafe: " << d.getMeters() << "m, "
              << "Agirlik: " << w.getKg() << "kg" << std::endl;
}

int main() {
    // calculateForce(75.0, 100.0);  // HATA! Artik derlenmez!

    // Dogru kullanim - niyetin acik:
    calculateForce(Distance(100.0), Weight(75.0));  // OK

    return 0;
}

Artık yanlışlıkla parametreleri karıştırman imkansız. Kodu okuyan herkes ne olduğunu açıkça görüyor. Bu, "strong typing" (güçlü tip sistemi) yaklaşımının güzel bir örneği.


Ne Zaman explicit Kullanmalı?

Altın kural: Tek parametreli (veya varsayılan parametreli) constructor'lara varsayılan olarak `explicit` koy.

Sadece implicit dönüşümün gerçekten mantıklı ve istenen bir davranış olduğu durumlarda explicit'i kaldır. Bu durumlar çok nadirdir.

explicit Koymalısın ✅

explicit String(int size);              // Boyut -> String? Mantıksız implicit.
explicit Vector(int capacity);          // Kapasite -> Vector? Tehlikeli.
explicit FilePath(const std::string&);  // String -> FilePath? Belirsiz.
explicit Money(double amount);          // double -> Money? Kur bilgisi yok!
explicit DatabaseConnection(const std::string& connStr);  // Config -> Connection?

explicit Koymayabilirsin ❌ (Nadir Durumlar)

// std::string'in const char* constructor'i explicit DEGIL - bilinçli karar
// Cunku "hello" -> std::string donusumu dogal ve beklenen
std::string s = "merhaba";  // Bu dogal hissettiriyor

// Complex sayi sinifi - double'dan donusum mantikli olabilir
// complex<double> c = 3.14;  // Reel sayi -> kompleks sayi

Ama genel kural olarak, şüphen varsa explicit koy. Sonradan kaldırmak, sonradan eklemekten (API'yi kırar) çok daha kolay.

⚠️ Dikkat: explicit sadece constructor'larda ve operator dönüşüm fonksiyonlarında kullanılır. Normal fonksiyonlarda veya destructor'da kullanamazsın. Ayrıca copy constructor'a explicit koymak çoğu zaman anlamsızdır ve kopya semantiğini bozar.


explicit ve Operator Dönüşüm Fonksiyonları

explicit sadece constructor'lar için değil, dönüşüm operatörleri (conversion operators) için de kullanılabilir:

#include <iostream>

class SafeBool {
private:
    bool value;

public:
    SafeBool(bool v) : value(v) {}

    // explicit olmadan: tehlikeli!
    // operator bool() const { return value; }
    // SafeBool x(true);
    // int n = x;      // bool -> int donusumu! 1 olur.
    // x + 5;          // Bu ne anlama geliyor?!

    // explicit ile: guvenli
    explicit operator bool() const { return value; }
};

int main() {
    SafeBool flag(true);

    // if icinde kullanim - explicit bile olsa calisir!
    // Cunku if/while/for "contextual conversion to bool" destekler
    if (flag) {
        std::cout << "Flag true!" << std::endl;
    }

    // Ama bunlar derlenmez:
    // int n = flag;        // HATA: explicit
    // bool b = flag;       // HATA: explicit
    // flag + 5;            // HATA: explicit

    // Acik donusum gerekir:
    bool b = static_cast<bool>(flag);  // OK
    std::cout << "b = " << b << std::endl;

    return 0;
}

explicit operator bool() özellikle önemli. std::optional, std::shared_ptr gibi standart kütüphane sınıfları bunu kullanır. if (ptr) çalışır ama int x = ptr derlenmez.


explicit(bool) — C++20 Koşullu explicit

C++20 ile birlikte explicit bir bool parametresi alabilir hale geldi. Bu, template'lerde koşullu explicit tanımlamak için kullanılır.

#include <iostream>
#include <type_traits>

template<typename T>
class Wrapper {
private:
    T value;

public:
    // T, int'e implicit donusebiliyorsa -> explicit degil
    // T, int'e donusemiyorsa -> explicit
    explicit(!std::is_convertible_v<T, int>) Wrapper(T v) : value(v) {}

    T get() const { return value; }
};

int main() {
    // int, int'e donusebilir -> explicit DEGIL
    Wrapper<int> w1 = 42;       // OK: implicit donusum

    // std::string, int'e donusemez -> explicit
    // Wrapper<std::string> w2 = "hello";  // HATA!
    Wrapper<std::string> w2(std::string("hello"));  // OK: acik

    return 0;
}

explicit(true) her zaman explicit demek, explicit(false) hiçbir zaman explicit değil demek. Aradaki koşullu durumlar template metaprogramming'de çok kullanışlı.

Bu C++20 özelliği günlük kodda nadir kullanılır ama kütüphane yazıyorsan bilmen gereken güçlü bir araç. Standart kütüphanede std::pair, std::tuple gibi sınıflarda kullanılıyor.


Pratik Örnek 1: Para Birimi Sınıfı

#include <iostream>
#include <string>
#include <stdexcept>

class Money {
private:
    long cents;  // Kuruslari tutuyoruz (floating point hatalarindan kacmak icin)
    std::string currency;

public:
    // explicit: double'dan otomatik donusum tehlikeli!
    // 100.0 ne? Dolar mi, Euro mu, TL mi?
    explicit Money(long c, const std::string& cur = "TRY")
        : cents(c), currency(cur) {}

    // Factory method'lar - niyeti acik
    static Money fromTL(double amount) {
        return Money(static_cast<long>(amount * 100), "TRY");
    }

    static Money fromUSD(double amount) {
        return Money(static_cast<long>(amount * 100), "USD");
    }

    Money operator+(const Money& other) const {
        if (currency != other.currency) {
            throw std::runtime_error("Farkli para birimleri toplanamaz!");
        }
        return Money(cents + other.cents, currency);
    }

    void print() const {
        std::cout << cents / 100 << "." << (cents % 100 < 10 ? "0" : "")
                  << cents % 100 << " " << currency << std::endl;
    }
};

void pay(Money amount) {
    std::cout << "Odeme: ";
    amount.print();
}

int main() {
    // pay(100.0);              // HATA! explicit engeller
    // pay(Money(10000));       // Belirsiz: 100 TL mi?

    auto price = Money::fromTL(49.99);    // Net: 49.99 TL
    auto shipping = Money::fromTL(9.90);  // Net: 9.90 TL

    auto total = price + shipping;
    pay(total);  // OK

    auto usd = Money::fromUSD(10.00);
    // auto invalid = price + usd;  // Runtime hata: farkli para birimleri!

    return 0;
}

explicit sayesinde pay(100.0) gibi anlamsız çağrılar engelleniyor. Factory method'larla (fromTL, fromUSD) niyetimiz çok açık.


Pratik Örnek 2: FilePath Sınıfı

#include <iostream>
#include <string>
#include <filesystem>

class FilePath {
private:
    std::string path;

public:
    // explicit: herhangi bir string otomatik path olmasin
    explicit FilePath(const std::string& p) : path(p) {
        // Path validasyonu yapilabilir
        if (path.empty()) {
            throw std::invalid_argument("Path bos olamaz!");
        }
    }

    // String'e donusum de explicit
    explicit operator std::string() const { return path; }

    std::string getPath() const { return path; }

    FilePath operator/(const std::string& subpath) const {
        return FilePath(path + "/" + subpath);
    }
};

void readFile(FilePath path) {
    std::cout << "Dosya okunuyor: " << path.getPath() << std::endl;
}

void processName(const std::string& name) {
    std::cout << "Isim isleniyor: " << name << std::endl;
}

int main() {
    // readFile("/etc/config");           // HATA! string -> FilePath implicit yok
    readFile(FilePath("/etc/config"));    // OK: acikca FilePath

    FilePath root("/home/user");
    FilePath docs = root / "documents";   // OK: operator/ kullaniyoruz
    readFile(docs);

    // processName(root);                 // HATA! FilePath -> string implicit yok
    processName(static_cast<std::string>(root));  // OK: acik donusum

    return 0;
}

Hem constructor hem de dönüşüm operatörü explicit. Bu, tür güvenliğini (type safety) maksimum seviyeye çıkarır.


Copy Elision + explicit Birlikte

Bu iki konu bazen kesişir. Copy elision olduğu için bazı explicit kısıtlamalar farklı davranabilir:

class Token {
public:
    explicit Token(int id) {
        std::cout << "Token(" << id << ") olusturuldu" << std::endl;
    }

    Token(const Token&) = delete;  // Kopya yok
    Token(Token&&) = delete;       // Tasima yok
};

Token createToken() {
    return Token(42);  // C++17: OK! Mandatory copy elision
                       // Copy/move constructor gerekmez
}

int main() {
    Token t = createToken();  // C++17: OK!
    // Token t2 = Token(42);  // C++17: OK! (direct initialization, copy elision)
    // Token t3 = 42;         // HATA: explicit engeller (bu copy elision meselesi degil)

    return 0;
}

💡 İpucu: explicit ve copy elision farklı mekanizmalar. explicit, kaynak türden hedef türe dönüşümü kontrol eder. Copy elision, aynı türden kopyalamayı kontrol eder. İkisi birbirini etkilemez — sadece aynı kodda birlikte görünebilir.


Derleyici Davranışlarını Gözlemleme

Copy elision'ın gerçekten çalışıp çalışmadığını test etmek için şu yöntemleri kullanabilirsin:

#include <iostream>

class Tracker {
    static int count;
    int id;

public:
    Tracker() : id(++count) {
        std::cout << "Default  [" << id << "]" << std::endl;
    }
    Tracker(const Tracker& o) : id(++count) {
        std::cout << "Copy     [" << id << "] <- [" << o.id << "]" << std::endl;
    }
    Tracker(Tracker&& o) noexcept : id(++count) {
        std::cout << "Move     [" << id << "] <- [" << o.id << "]" << std::endl;
    }
    ~Tracker() {
        std::cout << "Destruct [" << id << "]" << std::endl;
    }
};
int Tracker::count = 0;

Tracker makeTracker() {
    return Tracker();  // RVO
}

Tracker makeNamedTracker() {
    Tracker t;         // NRVO adayi
    return t;
}

int main() {
    std::cout << "--- RVO ---" << std::endl;
    Tracker a = makeTracker();

    std::cout << "\n--- NRVO ---" << std::endl;
    Tracker b = makeNamedTracker();

    std::cout << "\n--- Temizlik ---" << std::endl;
    return 0;
}

Farklı derleme seçenekleriyle çalıştırıp sonuçları karşılaştır:

# Copy elision ile (varsayilan)
g++ -std=c++17 -O2 tracker.cpp -o tracker && ./tracker

# Copy elision olmadan (sadece non-mandatory)
g++ -std=c++17 -fno-elide-constructors tracker.cpp -o tracker && ./tracker

Bu egzersiz, derleyicinin arka planda ne yaptığını anlamak için çok değerli.


Özet

  • Copy elision, derleyicinin gereksiz kopya/taşıma işlemlerini atlamasıdır. Performans için kritik.

  • RVO (Return Value Optimization): Fonksiyondan geçici nesne döndürürken kopyalama atlanır. C++17'de garantili.

  • NRVO (Named RVO): İsimli değişken döndürürken de atlanabilir ama garanti değil.

  • Converting constructor: Tek parametreli constructor'lar implicit tür dönüşümü sağlar. Bu genellikle tehlikelidir.

  • explicit keyword: Implicit dönüşümü engeller. Tek parametreli constructor'lara varsayılan olarak koy.

  • explicit(bool) (C++20): Koşullu explicit — template'lerde dönüşüm davranışını türe göre kontrol etmek için kullanılır.