Az operátortúlterhelés (operator overloading) C++-specifikus eszköz:
C-ben nincs rá lehetőség. A lényeg nem az, hogy „szép szintaxist”
adjunk, hanem hogy a típus viselkedése kiszámítható maradjon. Egy jól
megtervezett operátor ugyanazt a jelentést hordozza, mint a beépített
megfelelője: a + b ne módosítsa a-t,
a += b igen; ++it adja vissza az új értéket,
it++ a régit; az if (x) csak akkor működjön,
ha a típusnak van értelmes „igaz/hamis” állapota. Az
erőforrás-életciklust kezelő típusokat különösen óvatosan kell kezelni:
az operátor ne rejtsen el allokációt, lezárást vagy más, a felhasználó
számára meglepő mellékhatást.
Operátortúlterhelésről akkor beszélünk, amikor egy
operator@ nevű függvényt deklarálunk, ahol @ a
túlterhelhető operátor jele. Pontos feltételek:
&& és ||
rövidzárlata viszont elvész: a túlterhelt változat
mindkét operandust kiértékeli (lásd lent).=, [], (),
->, valamint a konverziós operátorok. A ::,
., .*, ?:, sizeof,
typeid, alignof, noexcept és a
cast-operátorok nem terhelhetők túl.friend-ként definiált
függvény). A választás nem esztétikai kérdés: a bal oldali operandus
konverzióit, az overload resolution halmazát és a hozzáférést is
befolyásolja.A következő táblázat a leggyakoribb, konvencionális választásokat foglalja össze.
| Operátor | Ajánlott forma | Visszatérés | Megjegyzés |
|---|---|---|---|
+=, -=, *= |
tag | T& |
módosítja a bal oldalt, *this-t ad vissza |
+, -, * |
nem tag vagy hidden friend | T |
új értéket ad, ne referenciát |
[] |
tag | T& / const T& |
általában nem ellenőriz boundsot |
() |
tag | tetszőleges | callable objektum |
++ prefix |
tag | T& |
először növel, az új értéket adja |
++ postfix |
tag | T |
a régi értéket adja vissza |
= |
tag | T& |
csak tag lehet |
swap |
nem tag + tag swap |
void |
lehetőleg noexcept |
| konverzió | tag | nincs visszatérési típus | implicit csak indokolt esetben |
A döntés vezérelve: ha az operátor a bal oldali operandust módosítja,
és a bal oldal mindig a saját típusunk, akkor tagfüggvény a természetes
választás. Ilyen a +=, a -=, a prefix
++, az [] és a (). Ha viszont
szimmetrikus műveletről van szó, ahol mindkét oldal szerepelhet eltérő
típusból, vagy a bal oldali operandus nem a mi típusunk, akkor nem tag
függvény kell. Például std::cout << x esetén a bal
oldali operandus std::ostream, ezért az
operator<< nem lehet tagja a saját típusunknak.
A nem tag operátor lehetővé teszi, hogy a bal oldali operanduson is
történjen felhasználói konverzió. Ez hasznos lehet, de veszélyes is: ha
túl sok implicit konverzió van, az overload resolution nehezen
követhetővé válik. A „hidden friend” olyan friend függvény,
amelyet a class belsejében definiálunk. Nem tagja a classnak, de
hozzáfér a privát tagokhoz, és csak argumentumfüggő kereséssel (ADL)
található meg. Ez csökkenti a globális névtér szennyezését, és
szimmetrikus operátoroknál gyakran jó választás.
A friend önmagában még nem tesz egy függvényt taggá;
csak hozzáférési jogot ad. A hidden friend előnye, hogy a definíció a
class mellett van, mégsem vesz részt a szokásos, nem ADL-alapú
névkeresésben.
A bevett minta: a módosító műveletet (+=)
tagfüggvényként írjuk meg, a bináris aritmetikát (+) pedig
arra építjük. A + bal oldali operandusát érték szerint
vesszük át, így a függvényen belül módosíthatjuk, majd értékként
visszaadjuk. Ez elkerüli a kézi duplikálást, és jól viselkedik akkor is,
ha a művelet kivételt dob: az eredeti objektum változatlan marad.
#include <iostream>
#include <utility>
class Money {
public:
using cents_t = long long;
constexpr Money() noexcept : cents_(0) {}
constexpr explicit Money(cents_t cents) noexcept : cents_(cents) {}
constexpr cents_t cents() const noexcept { return cents_; }
constexpr Money& operator+=(const Money& rhs) noexcept {
cents_ += rhs.cents_;
return *this;
}
constexpr Money& operator-=(const Money& rhs) noexcept {
cents_ -= rhs.cents_;
return *this;
}
Money& operator++() noexcept { // prefix
++cents_;
return *this;
}
Money operator++(int) noexcept { // postfix
Money old = *this;
++*this;
return old;
}
void swap(Money& other) noexcept {
std::swap(cents_, other.cents_);
}
private:
cents_t cents_;
};
constexpr Money operator+(Money lhs, const Money& rhs) noexcept {
lhs += rhs;
return lhs;
}
constexpr Money operator-(Money lhs, const Money& rhs) noexcept {
lhs -= rhs;
return lhs;
}
inline void swap(Money& a, Money& b) noexcept {
a.swap(b);
}
int main() {
Money a(1000);
Money b(250);
Money c = a + b; // 1250
c += Money(50); // 1300
Money d = c++; // d = 1300, c = 1301
++c; // c = 1302
std::cout << d.cents() << ' ' << c.cents() << '\n';
}Fordítási példa:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 money_demo.cpp -o money_demoA példa szándékosan nem használ implicit double
konverziót. A pénzösszeg belső reprezentációja egész cent, így nincs
lebegőpontos kerekítési hiba. A swap tagfüggvény és a nem
tag swap együtt biztosítja, hogy az ADL-alapú
swap(a, b) a saját, hatékony változatunkat találja meg. A
swap csak akkor legyen noexcept, ha a benne
végzett műveletek valóban nem dobnak; hamis noexcept esetén
a program std::terminate-hez juthat.
Az operator[] tipikusan tagfüggvény, és referenciát ad
vissza. Mutable objektumon T&, const objektumon
const T& a helyes visszatérési típus. A
bounds-ellenőrzés nem automatikus: a
std::vector::operator[] például nem ellenőriz, a
.at() igen. Saját konténer esetén dokumentálni kell az
előfeltételt: i < size(), és a megsértése nem definiált
viselkedés, ha a mögöttes tároló így viselkedik.
#include <cstddef>
#include <vector>
class IntBuffer {
public:
explicit IntBuffer(std::size_t n) : data_(n) {}
int& operator[](std::size_t i) noexcept { return data_[i]; }
const int& operator[](std::size_t i) const noexcept { return data_[i]; }
std::size_t size() const noexcept { return data_.size(); }
private:
std::vector<int> data_;
};A operator() callable objektumot készít. Állapotot
tárolhat, és a const minősítés itt is számít: ha a hívás
nem módosítja az objektumot, legyen const.
struct Scale {
double factor;
double operator()(double x) const noexcept { return factor * x; }
};Használat: Scale s{2.0}; double y = s(3.0);. A call
operátort könnyű túlhasználni; ha a műveletnek nem természetes a
„függvényhívás” jelentése, gyakran beszédesebb egy névvel ellátott
tagfüggvény.
A prefix ++ visszatérése T&: a művelet
után az objektum új állapotára mutató referenciát ad. A postfix
++ szignatúrája T operator++(int): a dummy
int különbözteti meg a prefix változattól, és a
visszatérési típus érték, nem referencia. A postfixnek a módosítás
előtti értéket kell visszaadnia, ezért jellemzően egy másolatot készít,
majd megnöveli az eredetit. Emiatt a postfix drágább lehet; generikus
kódban ezért preferált a ++it.
A postfix T&-et visszaadó változata tipikus hiba: ha
lokális old objektum referenciáját adja vissza, a hívó
dangling referenciát kap. Ha az eredeti objektumot adja vissza, akkor
nem a régi értéket képviseli, hanem az újat, ami szintén meglepő.
A copy assignment és a move assignment hagyományos szignatúrája:
T& operator=(const T& other);
T& operator=(T&& other) noexcept;Mindkettő tagfüggvény, és *this-t ad vissza. A
láncolhatóság (a = b = c) és a generikus algoritmusok miatt
a void visszatérés kerülendő. Önmagára értékadásnál
(a = a) az implementációnak helyesen kell működnie;
copy-and-swap esetén ez általában automatikusan teljesül. A move
assignment után a forrás „moved-from” állapotba kerül: érvényes, de nem
specifikált állapotú; csak destruálás vagy újabb értékadás biztonságos
vele kapcsolatban. Erről bővebben a Move
constructor, moved-from állapot és std::move rész szól.
A compound assignment (+=, -=)
tagfüggvényként, T&-et visszaadva a legtermészetesebb.
A bináris + erre épülhet, ahogy a Money
példában. A swap nem tag függvénye ugyanabban a névtérben
legyen, mint a típus, hogy az ADL megtalálja; a std::swap
specializálása helyett ez a javasolt minta. A kivételbiztonsági
garanciákról és a copy-and-swapról a noexcept, swap
és container-reallocation rész ad részletesebb képet.
A konverziós operátor tagfüggvény, nincs visszatérési típusa, és a
neve operator T. Például:
struct FdView {
int fd = -1;
explicit operator bool() const noexcept {
return fd >= 0;
}
};Az explicit operator bool a „safe bool” modern
megfelelője. Kontextuális konverzióként működik if,
while, !, &&,
||, ?: feltételében és
static_assert-ben, de nem enged meg olyan implicit
átalakítást, mint bool b = v; vagy int n = v;.
Ez pontosan az, amit egy „érvényes/érvénytelen” állapotú típusnál
szeretnénk. A régi operator void* trükk helyett C++11 óta
ezt kell használni.
Általános szabály: implicit konverziós operátort csak akkor adjunk,
ha a konverzió tényleg veszteségmentes és a típus természetes része.
Ellenkező esetben explicit legyen, vagy adjunk nevesített
hozzáférőt (to_int(), celsius()). Az implicit
operator int vagy operator double könnyen
váratlan overload resolutionhöz és több lépéses konverziós lánchoz
vezet. Egy erőforrás-tulajdonos típusnál az implicit bool különösen
veszélyes, mert nem mondja meg, hogy „érvényes”-e vagy „nem nulla”-e a
jelentés.
Az operátortúlterhelés nem változtatja meg a precedenciát és az
asszociativitást. Ha a + b * c szerepel, a *
továbbra is erősebb, akkor is, ha saját operator+-t és
operator*-ot definiálunk. Nem lehet „jobbról balra
asszociatív” operator+-t csinálni, és nem lehet
megváltoztatni az operandusok számát sem.
A && és || túlterhelése különösen
veszélyes, de két különböző okból, amelyeket érdemes szétválasztani.
&&
rövidzáras: ha a bal oldal hamis, a jobb oldal nem értékelődik ki. A
túlterhelt változat függvényhívás, ezért mindkét operandus
mindig kiértékelődik. Ez minden szabványverzióban így
van.&&, || és ,
operátorszintaxissal (a && b) hívva megőrzi a
beépített operátor sorrendi szabályát: a bal operandus a jobb előtt
értékelődik ki, mellékhatásaival együtt ([C++20] [over.match.oper],
[expr.call]). C++14-ben és korábban ez nem volt garantált. Explicit
függvényhívás-szintaxisnál (operator&&(a, b))
viszont az argumentumok sorrendje továbbra sem meghatározott.// operator_order.cpp — C++17/C++20: sorrend megmarad, rövidzár elvész
#include <cstdio>
struct Flag { bool value; };
bool operator&&(Flag a, Flag b) { return a.value && b.value; } // kerülendő
Flag operator,(Flag, Flag b) { return b; } // kerülendő
int counter = 0;
Flag left() { ++counter; std::puts("left"); return Flag{false}; }
Flag right() { std::printf("right: counter=%d\n", counter); return Flag{true}; }
int main() {
counter = 0;
bool r = left() && right(); // operátorszintaxis: left előbb, right is fut
std::printf("&& -> %d\n", r);
counter = 0;
Flag f = (left(), right()); // ugyanígy a vesszőnél
std::printf(", -> %d\n", f.value);
// Explicit függvényhívás-szintaxisnál az argumentumok sorrendje
// nem meghatározott: operator&&(left(), right())
}Buildjegyzőkönyv (lab-gcc profil,
-std=c++20). A kimenet: left,
right: counter=1, && -> 0, majd
ugyanez a vesszőre , -> 1. A jobb operandus látja a bal
mellékhatását, és a hamis bal oldal ellenére is lefut. (A GCC
-std=c++14 módban is ezt a sorrendet adta, de ott ez nem
garantált.)
Logikai rövidzárlatot igénylő kódban ezért ne terheljük túl a
&&, || és ,
operátorokat.
Ha mégis szükség van logikai műveletre, használjunk nevesített
függvényt (both(), either()), vagy hagyjuk a
beépített operátort érvényesülni explicit operator bool
segítségével.
operator+ referenciát ad vissza
lokálisra. A bináris aritmetika értéket adjon vissza, ne
T&-et. A T& gyakran dangling
referenciához vezet.++ T&-et ad
vissza. Helyesen T operator++(int), a régi érték
másolatával.void-ot ad vissza.
Helyesen T&, *this-szel.operator[] értéket ad vissza mutable
konténeren. Ettől nem lehet v[i] = x alakban írni.
Mutable változatban T&, const változatban
const T& kell.operator<< tagfüggvény. A bal
oldali operandus std::ostream, ezért nem tag függvényként
kell definiálni.operator bool vagy
operator int. Váratlan konverziókhoz vezet;
használjunk explicit-et vagy nevesített függvényt.operator&&.
Elveszik a rövidzárlat; ne tegyük, ha a logikai jelentés számít.swap nem noexcept, mégis annak
deklaráljuk. Ha a benne hívott művelet dob, a
noexcept megsértése std::terminate-hez
vezet.Az operátortúlterhelés akkor jó, ha a típus viselkedése a beépített
operátorok természetes analógiáját követi. A módosító műveletek
tagfüggvényként T&-et adnak vissza; a bináris
aritmetika értéket ad; az indexelés referenciát; a postfix
++ a régi értéket. A swap nem tag
függvényként, ADL-lel találhatóan, lehetőleg noexcept
legyen. A konverziós operátorokat explicit-té kell tenni,
különösen a bool-t. Az operátorok nem változtatják meg a
precedenciát, és a túlterhelt &&/||
nem őrzi meg a rövidzárlatot. Az osztálytervezésben ezért az operátor a
publikus szerződés része, nem trükk.
Adott egy Temperature értéktípus, amely Celsius-fokot
tárol double-ként. Tervezd meg a += és
+ operátorokat, és indokold, melyik legyen tag, melyik nem
tag. A hőmérséklet kiolvasására ne implicit double
konverziót használj, hanem nevesített hozzáférőt; ha mégis konverziót
adsz, indokold, miért explicit.
Implementálj egy Counter osztályt, amelynek van prefix
++ és postfix ++ operátora. A postfix adja
vissza a módosítás előtti értéket. Írj rövid main-t, amely
ellenőrzi a viselkedést, és add meg a visszatérési típusokat. Magyarázd
el, miért nem adhat a postfix Counter&-et vissza.
Az alábbi kód több operátorral kapcsolatos hibát tartalmaz. Találj legalább négyet, indokold őket, és írd meg a javított változatot.
struct Box {
int v;
operator bool() const { return v != 0; }
Box& operator+(const Box& other) { v += other.v; return *this; }
Box& operator++(int) { Box old = *this; ++v; return old; }
};
bool operator&&(Box a, Box b) { return a.v && b.v; }A += módosítja a bal oldali operandust, ezért
tagfüggvény, és Temperature&-et ad vissza. A
+ szimmetrikus, új értéket állít elő, ezért nem tag
függvény vagy hidden friend, és Temperature-t ad vissza
értékként. A nevesített celsius() hozzáférő egyértelműbb,
mint az implicit double konverzió; ha mégis kell konverzió,
az explicit operator double() const legyen, hogy csak
static_cast<double>(t) alakban lehessen
használni.
class Temperature {
public:
explicit Temperature(double celsius) noexcept : celsius_(celsius) {}
double celsius() const noexcept { return celsius_; }
Temperature& operator+=(const Temperature& rhs) noexcept {
celsius_ += rhs.celsius_;
return *this;
}
private:
double celsius_;
};
inline Temperature operator+(Temperature lhs, const Temperature& rhs) noexcept {
lhs += rhs;
return lhs;
}Ellenőrzési feltétel:
Temperature t{20.0}; t += Temperature{2.0}; auto u = t + Temperature{3.0};
után t.celsius() == 22.0 és
u.celsius() == 25.0. A double d = u; ne
forduljon le, ha nincs implicit konverzió;
static_cast<double>(u) csak akkor működjön, ha az
explicit konverziót külön hozzáadtuk.
class Counter {
public:
constexpr Counter() noexcept : value_(0) {}
constexpr explicit Counter(int value) noexcept : value_(value) {}
constexpr int value() const noexcept { return value_; }
Counter& operator++() noexcept { // prefix
++value_;
return *this;
}
Counter operator++(int) noexcept { // postfix
Counter old = *this;
++*this;
return old;
}
private:
int value_;
};A postfix azért ad vissza értéket, mert a módosítás előtti állapotot
kell reprezentálnia. Ha Counter&-et adna vissza, vagy a
lokális old-ra mutató dangling referenciát adna, vagy az
eredetit adná vissza, ami már az új értéket képviseli. Ellenőrzési
feltétel: Counter c; Counter a = c++; Counter b = ++c; után
a postfix eredménye a régi, a prefix eredménye az új érték; a
c állapota mindkét művelet után megfelelően nő.
A hibák:
operator bool implicit, ezért váratlan
bool, int és egyéb konverziókhoz vezethet.
Legyen explicit operator bool() const noexcept.operator+ módosítja a bal oldali operandust, és
referenciát ad vissza. A bináris + ne módosítson, és
értéket adjon vissza; a módosítást += végezze.operator++ Box&-et ad vissza
lokális old-ra, ami dangling referencia. Helyesen
Box operator++(int).operator&& elveszíti a
rövidzárlatot. Törölni kell; az explicit operator bool
mellett a beépített && használható.Box v tagja inicializálatlan lehet;
adjunk neki alapértéket.struct Box {
int v = 0;
explicit operator bool() const noexcept { return v != 0; }
Box& operator+=(const Box& other) noexcept {
v += other.v;
return *this;
}
Box& operator++() noexcept {
++v;
return *this;
}
Box operator++(int) noexcept {
Box old = *this;
++*this;
return old;
}
};
inline Box operator+(Box lhs, const Box& rhs) noexcept {
lhs += rhs;
return lhs;
}Ellenőrzési feltétel: Box a{1}, b{2}; Box c = a + b;
után a.v változatlan, c.v == 3;
auto d = c++; esetén d.v a régi,
c.v az új érték; if (a) és
a && b a beépített rövidzárlatos logikát használja;
bool x = a; nem fordul le. A javított kódban a
+ nem módosít mellékhatásként, a postfix nem ad vissza
dangling referenciát, és a logikai operátorok viselkedése nem változik
meg.