İçeriğe geç
academia.sh

Ders 07 / 14

İşleç Aşırı Yükleme

operator+'nın neden yeni bir geçici nesne döndürdüğünü, operator+='in neden bunun yerine kendi nesnesini referansla döndürdüğünü ölçer.

Yayın Güncelleme

Bu derste

  • operator+'nın yeni bir geçici nesne döndürdüğünü ölçer
  • operator+='in kendi nesnesini değiştirip referansla döndürdüğünü ayırt eder
  • Bir sınıf için hangi işleçlerin anlamlı olduğuna karar verirken sınırları uygular
İçindekiler

Bu, K04’ün bu bölümdeki son dersidir. Önceki ders bir referansın hiçbir kopya üretmeden çağıranın kendi nesnesiyle çalıştığını ölçtü. Bu ders, bir işlecin (operator) bazen tam tersini yaptığını — yeni bir nesne ürettiğini — ölçer. Ölçüm sınıfı geçici: a + b ifadesinin sonucu, ne a ne b’dir; işlecin gövdesinde kurulan, ifadenin sonunda yok olan yeni bir Para nesnesidir.

C’de böyle bir toplamanın tek yolu adı açık bir işlevdi: Para toplama_yap(Para a, Para b) gibi bir işlev çağrılır, sonucu ayrı bir değişkene atanırdı — a + b yazmak mümkün değildi, çünkü + yalnız yerleşik sayısal tipler için tanımlıydı. C++’ta operator+ aşırı yüklemesi, Para gibi kullanıcı tanımlı bir tip için de + sözdizimini kullanıma açar; çağıran taraf toplama_yap(a, b) yerine a + b yazar. Bu, sözdiziminin okunurluğunu artırır, ama gövdenin ne yaptığını (yeni bir nesne mi kurduğunu, var olan bir nesneyi mi değiştirdiğini) gizler de — bu yüzden bu dersin ölçümü tam olarak bunu, işlecin sözdiziminin arkasında hangi nesnenin üretildiğini, açığa çıkarır.

İşleç Aşırı Yükleme Nedir

İşleç aşırı yükleme (operator overloading), +, ==, << gibi yerleşik işleçlerin kullanıcı tanımlı bir sınıf için ne anlama geldiğini tanımlamaktır. operator+ bir üye işlev olarak yazıldığında, a + b ifadesi derleyici tarafından a.operator+(b) olarak okunur — sözdizimi + kalır, ama çağrılan şey sınıfın kendi yazdığı bir işlevdir. Aşırı yükleme yeni bir sözdizimi eklemez; var olan işleç sözdizimini kullanıcı tanımlı tipler için anlamlandırır.

Aşırı yükleme bir işlecin kaç argüman aldığını ya da öncelik sırasını değiştiremez — + her zaman ikili kalır, *’ten sonra değerlendirilmeye devam eder. Değişebilen tek şey işlecin argümanları için ne yaptığıdır. Bu sınır kasıtlıdır: operator+’yı çağıran biri, Para için de tıpkı int için olduğu gibi “iki değeri alıp bir sonuç üretir” davranışını bekler; aşırı yükleme bu beklentiyi bozacak bir sözdizimi değişikliği yapamaz, yalnız “sonuç” ve “değiştirme” kısmının ne anlama geldiğini tanımlayabilir.

// para.hpp — kurus cinsinden tutar tutan, islec asiri yuklemesi icin bir sinif
#ifndef PARA_HPP
#define PARA_HPP

#include <cstdio>

class Para {
public:
    explicit Para(long kurus) : kurus_(kurus) {}

    Para operator+(const Para& digeri) const {
        return Para(kurus_ + digeri.kurus_);
    }

    Para& operator+=(const Para& digeri) {
        kurus_ += digeri.kurus_;
        return *this;
    }

    bool operator==(const Para& digeri) const {
        return kurus_ == digeri.kurus_;
    }

    long kurus() const { return kurus_; }

private:
    long kurus_;
};

inline void yaz_para(const Para& p) {
    std::printf("kurus=%ld\n", p.kurus());
}

#endif

operator+: Yeni Bir Geçici Döndürür

// artı-olcumu.cpp — a + b sonucu yeni bir gecici mi
#include <cstdio>

#include "para.hpp"

int main() {
    Para a(100);
    Para b(250);

    Para c = a + b;

    std::printf("c ile a ayni nesne mi %s\n", &c == &a ? "evet" : "hayir");
    std::printf("c ile b ayni nesne mi %s\n", &c == &b ? "evet" : "hayir");
    yaz_para(a);
    yaz_para(b);
    yaz_para(c);
    return 0;
}
c ile a ayni nesne mi hayir
c ile b ayni nesne mi hayir
kurus=100
kurus=250
kurus=350

SK15. a + b ifadesi a.operator+(b) çağrısına dönüşür; operator+’nın gövdesi return Para(kurus_ + digeri.kurus_); ile yeni bir Para kurar ve bunu değerle döndürür — a’yı da b’yi de değiştirmez, ikisi de kendi kurus_ değerini korur (100 ve 250). Döndürülen nesne c’ye kurulur; c’nin adresi ne a’nınkiyle ne b’ninkiyle aynıdır. operator+’nın const işaretli olması (operator+(const Para& digeri) const) hem digeri’nin hem çağrıldığı nesnenin (this) değiştirilmediğini garanti eder — bir toplama işleci, tanım gereği, toplananları değil yalnız sonucu üretmelidir.

operator+=: Kendi Nesnesini Değiştirip Referansla Döner

// arti-esittir-olcumu.cpp — a += b, a'nin kendisini mi degistiriyor
#include <cstdio>

#include "para.hpp"

int main() {
    Para a(100);
    Para b(250);

    Para& sonuc = (a += b);

    std::printf("sonuc ile a ayni nesne mi %s\n", &sonuc == &a ? "evet" : "hayir");
    yaz_para(a);
    return 0;
}
sonuc ile a ayni nesne mi evet
kurus=350

SK16. operator+=’in gövdesi kurus_ += digeri.kurus_; ile kendi kurus_ üyesini değiştirir, sonra return *this; ile kendisini referansla döndürür — yeni bir nesne kurmaz. a += b çağrıldığında a’nın kendisi değişir (350 olur) ve (a += b)’nin sonucu da yine a’nın kendisidir: &sonuc == &a doğrudur. Bu, operator+ ile taban tabana zıttır: operator+ iki toplananı da değiştirmeden yeni bir geçici üretirken, operator+= tek bir nesneyi (soldaki) değiştirip kendisi sınıfında bir referans döner. İki işlecin dönüş tipi bu farkı imzada taşır — operator+ değerle (Para), operator+= referansla (Para&) döner; imza okunarak hangisinin yeni bir nesne ürettiği, hangisinin var olan bir nesneyi değiştirdiği önceden bilinebilir.

Anlamlı İşleç Tanımlamanın Sınırları

Bir işlecin kullanıcı tanımlı bir sınıf için aşırı yüklenebiliyor olması, her sınıfın her işleci tanımlaması gerektiği anlamına gelmez. Birkaç yerleşik kural işleç tanımlarını tutarlı tutar:

  • +, -, * gibi ikili aritmetik işleçler genellikle üye değil serbest işlev (free function) olarak yazılır (Para operator+(const Para& a, const Para& b)), çünkü bu, sol tarafı Para olmayan ifadelerde de (örneğin gelecekte bir int ile Para toplanacaksa) simetrik dönüşüm sağlar. Para bu dersin ölçümü için operator+’yı üye olarak tanımlar; serbest işlev biçimi yalnız burada anılır.
  • +, += üzerinden yazılabilir: operator+’nın gövdesi kurus_ + digeri.kurus_ yerine Para sonuc(kurus_); sonuc += digeri; return sonuc; biçiminde de yazılabilirdi — +=’in mantığını tekrar etmeden +’yı ona dayandırmak, iki işlecin davranışının birbirinden sapmasını engeller. Para bu dersin ölçümünü sade tutmak için ikisini bağımsız yazar.
  • == bir eşitlik, bir sıralama değildir; Para::operator== iki nesnenin kurus_ değerlerini karşılaştırır, < gibi bir sıralama işleci bu derste tanımlanmaz.
  • << (akış çıktısı) bir üye işlev olamaz — std::cout << p ifadesinde sol taraf std::ostreamdur, Para değil; bu yüzden operator<< bir serbest işlev olarak yazılır ve Para’nın private üyesine erişmesi gerekiyorsa friend bildirimiyle (ikinci derste yalnız adıyla anılmıştı) izin verilir. Bu ders operator<< tanımlamaz, yaz_para adında sıradan bir işlevle aynı işi görür.

Bir işleci aşırı yüklerken sorulması gereken soru şudur: bu işlecin yerleşik anlamı (+ toplar, == karşılaştırır, << yazdırır) sınıf için hâlâ doğal mı? Para için + toplamı, == eşitliği anlamlı biçimde karşılar; ama örneğin Para operator*(const Para& a, const Para& b) iki tutarı çarpmanın parasal bir anlamı olmadığı için tanımsız bırakılabilirdi — bu ders böyle bir işleç yazmaz.

Bu sınırın ihlali derlenir ama okunmaz kılar. C++ hiçbir işlecin hangi anlamda kullanılacağını zorlamaz — operator+’nın gövdesine teknik olarak bir çıkarma yazılabilir, derleyici buna itiraz etmez. Ama böyle bir kod, a + b yazan herkesin beklediği (“toplanıyor”) ile gerçekte olanı (“çıkarılıyor”) ayırır — bu, sınıfın kullanıcısını yanıltan, hatası derleme zamanında yakalanamayan bir tasarım kusurudur. Aşırı yükleme gücünü kısıtlayan tek şey derleyici değil, işlecin okuyucuda uyandırdığı beklentiye sadık kalma disiplinidir.

explicit, Kopyalayan Yapıcı ve İşleçlerin Buluştuğu Yer

Para’nın yapıcısı explicit Para(long kurus) olarak tanımlıdır — üçüncü dersteki explicit kuralı burada da geçerlidir: Para p = 100; gibi örtük bir dönüşüm derlenmez, Para p(100); ya da Para p{100}; yazılmalıdır. Bu, işleç aşırı yüklemesiyle doğrudan etkileşir: operator+’nın gövdesindeki return Para(kurus_ + digeri.kurus_); satırı açık bir kurma yazar; eğer yapıcı explicit olmasaydı ve gövde yalnız return kurus_ + digeri.kurus_; yazsaydı, long sonucu örtük olarak bir Para’ya dönüştürülebilirdi — bu, üçüncü dersin uyardığı türden, istenmeyen bir örtük dönüşüm zincirine kapı açardı (örneğin bir Para’nın yanlışlıkla bir longla, dolaylı yoldan, karşılaştırılabilmesi gibi). operator+’nın sonucu her zaman açıkça bir Para kurarak döndürülür; bu hem explicit kuralına uyar hem işlecin ne ürettiğini kod içinde açık tutar. Beşinci dersteki kopyalayan yapıcı da burada örtük olarak devreye girer: return Para(...) satırı yeni bir Para değeri döndürdüğünde, derleyici çoğu durumda bu değeri doğrudan çağıranın alacağı yere kurar (değer kopyalamayı atlayan bir optimizasyon), ama kavramsal model hâlâ “yeni bir nesne kuruldu ve döndürüldü”dür — kopyalayan yapıcının hiç çağrılmaması bir optimizasyon ayrıntısıdır, operator+’nın yeni bir nesne ürettiği gerçeğini değiştirmez.

Değer Semantiği: Bu Kursun Bağlamı

M11/K01’de bir yapının atamayla bütün alanlarıyla kopyalandığı görülmüştü; bu, değer semantiğidir (value semantics): bir değer atandığında ya da aktarıldığında, iki taraf birbirinden bağımsız hâle gelir. Para bu ilkeyi C++’ın kendi araçlarıyla — kopyalayan yapıcı, kopyalayan atama, ve şimdi operator+’nın yeni bir değer döndürmesiyle — uygular. operator+= ise kasıtlı olarak bu ilkenin dışına çıkar: kendi nesnesini değiştirir, çünkü +=’in yerleşik anlamı zaten “soldaki değeri güncelle”dir (x += 1 de x’i değiştirir, yeni bir değişken kurmaz). Bir sınıf için işleç yazarken bu iki davranış biçiminden hangisinin seçildiği, işlecin yerleşik int/long karşılığının davranışıyla tutarlı olmalıdır — Para bu tutarlılığı korur: + yeni bir değer üretir, += var olanı değiştirir, tıpkı int için olduğu gibi.

Kopya, Geçici ve Kendisi Ayrımının Özeti

Bu konunun yedi dersi tek bir soruyu farklı açılardan ölçtü: bir ifade hangi nesneyle çalışıyor? Üçüncü ve dördüncü dersler bir nesnenin kendi kaynağını nasıl aldığını (yapıcı, yıkıcı, RAII); beşinci ders bir kopyanın orijinalinden nasıl ayrıştığını (kopya); altıncı ders bir referansın hiç kopyalamadan çağıranın kendisiyle nasıl çalıştığını; bu ders ise bir işlecin ne zaman yeni bir geçici ürettiğini, ne zaman kendi nesnesini değiştirip kendisini döndürdüğünü ölçtü. Dört sınıf — kendisi, kopya, parça, geçici — bir C++ ifadesini okurken sorulacak tek soruya karşılık gelir: bu ifadenin ürettiği ya da işlediği nesne, başka hangi nesneyle aynı, ve hangisinden farklı?

Sınırlayıcı Ölçüm

// zincirleme-olcumu.cpp — a + b + c kac gecici uretir
#include <cstdio>

#include "para.hpp"

int main() {
    Para a(10);
    Para b(20);
    Para c(30);

    Para toplam = a + b + c;

    yaz_para(toplam);
    return 0;
}
kurus=60

SK17. a + b + c ifadesi soldan sağa değerlendirilir: önce a + b çalışır ve kurus_=30 değerinde adsız bir Para üretir; sonra bu sonuç + c çağrısının sol tarafı olur. operator+ bir üye işlev olduğu için ikinci çağrının this’i bir nesneyi göstermek zorundadır: a + b‘nin sonucu bu yüzden bir geçici olarak kurulur ve tam ifade (a + b + c;nin sonu, noktalı virgül) bitene kadar yaşar, sonra yıkılır. İkinci operator+ çağrısının kurus_=60 değerindeki sonucu ise ayrı bir geçici olarak kurulmaz: C++17’nin garantili kopya seçimi (guaranteed copy elision) gereği doğrudan toplamın kendi deposunda kurulur. İki operator+ çağrısı, dolayısıyla bir geçici; toplam ikinci çağrının sonucunun ta kendisidir, bir kopyası değil. Para’nın yıkıcısı burada bir printf içermediği için geçicinin kurulma ve yıkılma anı bu blokta gözlemlenmiyor; sayı dilin kuralından okunur, bu ölçümden değil. Zincirleme bir toplamanın “kaç geçici ürettiği” sorusunun cevabı, işlecin kaç kez çağrıldığından çok, her sonucun nereye gittiğine bağlıdır: bir sonraki çağrının nesnesi olan ara sonuç geçicidir, bir değişkeni ilklendiren son sonuç o değişkenin kendisidir.

Özet

  • Bu dersin ölçtüğü ifade geçici sınıfındadır: operator+ toplananları değiştirmeden yeni ve adsız bir Para döndürür; dönüş tipi değerledir (Para, referans değil).
  • operator+= kendi nesnesini değiştirir ve kendisini (*this) referansla döndürür; dönüş tipi Para&dır.
  • İkili aritmetik işleçler genellikle serbest işlev olarak yazılır; << bir üye işlev olamaz çünkü sol tarafı sınıfın kendisi değildir.
  • Bir işleç yalnız yerleşik anlamı sınıf için doğal olduğunda tanımlanmalıdır.
  • Zincirleme bir ifadede (a + b + c) yalnız ara sonuç (a + b) geçici olarak kurulur ve tam ifade bitene kadar yaşar; son sonuç doğrudan toplamın içinde kurulur.
  • Bu konunun yedi dersi aynı soruyu sordu: bir C++ ifadesi kendisi, kopya, parça ya da geçici nesnelerden hangisiyle çalışıyor?

Sonraki Adım

Bu konu, bir nesnenin kendi kaynağını nasıl aldığını, kopyalandığını, referansla aktarıldığını ve işleçler aracılığıyla yeni nesneler ürettiğini ölçtü. Bir sonraki konu bu tek nesne modelinden uzaklaşıp sınıflar arasındaki ilişkiye — bir sınıfın başka bir sınıfın davranışını nasıl genişlettiğine ve çalışma zamanında hangi işlevin çağrılacağının nasıl belirlendiğine — geçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat