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ıParaolmayan ifadelerde de (örneğin gelecekte birintileParatoplanacaksa) simetrik dönüşüm sağlar.Parabu dersin ölçümü içinoperator+’yı üye olarak tanımlar; serbest işlev biçimi yalnız burada anılır.+,+=üzerinden yazılabilir:operator+’nın gövdesikurus_ + digeri.kurus_yerinePara 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.Parabu dersin ölçümünü sade tutmak için ikisini bağımsız yazar.==bir eşitlik, bir sıralama değildir;Para::operator==iki nesneninkurus_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 << pifadesinde sol tarafstd::ostreamdur,Paradeğil; bu yüzdenoperator<<bir serbest işlev olarak yazılır vePara’nınprivateüyesine erişmesi gerekiyorsafriendbildirimiyle (ikinci derste yalnız adıyla anılmıştı) izin verilir. Bu dersoperator<<tanımlamaz,yaz_paraadı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 birParadö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üş tipiPara&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ğrudantoplamı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.