Jobbérték referenciák
Czirkos Zoltán · 2025.09.02.
Tulajdonos szemantika és hatékonyság: mozgató konstruktorok és jobbérték referenciák alkalmazása.
A laborokhoz
A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.
Labor otthoni munkában
A labor teljesítéséhez mind a négy feladatot meg kell oldani.
A C++ számára minden objektum értéket jelent. A nyelv úgy tekinti, hogy egy objektum és annak másolata egyforma,
megkülönböztethetetlen – ahogyan az a beépített típusoknál is van. Ezért a szabvány azt mondja, hogy a fordítók számára
megengedett a másolatok számának csökkentése. Például ha egy függvényből objektummal térünk vissza, akkor a lokális változó
másolása és megszűntetése helyett ügyeskedhetnek azzal, hogy eleve a visszatérési érték számára fenntartott helyen hozzák létre
az objektumot. Ezt úgy nevezik, hogy „copy elision” vagy „return value optimization”, és a szabvány még akkor is megengedi, ha
a másoló konstruktornak mellékhatása van. A G++ és a Clang++ fordítóknál alapbeállítás szerint engedélyezve van ez az
optimalizációs lehetőség. Kikapcsolni a -fno-elide-constructors
paraméterrel lehet.
Vigyázat – a 3.4-es Clang fordító -fno-elide-constructors
paraméter mellett néha
hibás kódot generál! 3.5-ös verzióra már kijavították ezt a hibát.
Másold be a fejlesztőkörnyezetbe az alábbi kódrészletet! Ebben a szokásos, mindenért kiabáló osztály objektumait adogatjuk ide-oda függvényeknek, függvényekből. Futtasd le a kódot „copy elision” optimalizációval és anélkül! Magyarázd meg az összes függvény (és függvényhívás) futási eredményét!
#include <iostream>
class Noisy {
public:
Noisy() { std::cout << "Noisy ctor\n"; }
~Noisy() { std::cout << "Noisy dtor\n"; }
Noisy(Noisy const &) { std::cout << "Noisy copy\n"; }
Noisy(Noisy &&) noexcept { std::cout << "Noisy move\n"; }
};
Noisy create_noisy_on_return() {
return Noisy();
}
Noisy create_noisy_from_local() {
Noisy y;
return y;
}
Noisy create_noisy_with_move() {
Noisy y;
return std::move(y); /* ilyet nem írunk!!! de miért? */
}
void do_something_with_noisy_cref(Noisy const &y) {
}
void do_something_with_noisy_value(Noisy y) {
}
void do_something_with_noisy_rref(Noisy && y) {
}
int main() {
std::cout << "create_noisy_on_return()\n";
create_noisy_on_return();
std::cout << "create_noisy_from_local()\n";
create_noisy_from_local();
std::cout << "create_noisy_with_move()\n";
create_noisy_with_move();
std::cout << "do_something_with_noisy_cref()\n";
do_something_with_noisy_cref(Noisy());
std::cout << "do_something_with_noisy_value()\n";
do_something_with_noisy_value(Noisy());
std::cout << "do_something_with_noisy_rref()\n";
do_something_with_noisy_rref(Noisy());
}
Megoldás
A create_noisy_on_return()
és create_noisy_from_local()
függvényekben tud RVO-t
használni a fordító. Ha optimalizálhatja a másolásokat, akkor itt egyáltalán nem történik se másolás, se
mozgatás.
A create_noisy_with_move()
függvény egy tipikus kódolási hibát mutat. A lokális változóknál
nem kell külön engedélyezni a mozgatást a visszatérési értékben; ha nem engedélyezzük, akkor is mozgatás
történik, ha van ilyen konstruktor. Az std::move()
-ot kiírva viszont letiltjuk a visszatérési
érték optimalizációját, mivel azt csak akkor tudja alkalmazni a fordító, ha egy változónevet adunk meg a
return
utasításnál.
A do_something_with_noisy_cref()
függvénynél biztosan nem látunk másolást, mert referencia
paraméterű. A do_something_with_noisy_value()
függvénynek értékparamétere van, de lehet, hogy itt sem
látunk másolást. Mivel a jobbérték objektumnak nincs neve, biztos, hogy senki más nem látja, ezért
„elhasználható” a függvény paramétereként. Érdekes, hogy emiatt az optimalizáció miatt néha gyorsabb lehet
a program értékparaméterrel, mint referenciaparaméterrel.
Adott az alábbi osztály:
/* FILE* RAII */
class FilePtr {
public:
explicit FilePtr(FILE *fp = nullptr) : fp_{fp} {}
~FilePtr() {
close_if_open();
}
FilePtr & operator=(FILE *fp) {
close_if_open();
fp_ = fp;
return *this;
}
operator FILE* () const {
return fp_;
}
/* nem másolható */
FilePtr(FilePtr const &) = delete;
FilePtr & operator=(FilePtr const &) = delete;
private:
FILE *fp_;
void close_if_open() {
if (fp_ != nullptr)
fclose(fp_);
fp_ = nullptr;
}
};
Ez egy régebbi laborfeladat megoldása. A lényege az, hogy automatikus erőforráskezelést biztosít a
FILE*
-okhoz, azaz automatikusan bezárja a nyitott fájlokat.
int main() {
FilePtr fp;
/* nagyjából így használható: */
fp = fopen("hello.txt", "wt");
fprintf(fp, "Hello vilag");
}
A másoló konstruktora és másoló értékadó operátora le van tiltva, mert nem lehet két olyan FilePtr
, amely
ugyanazt a nyitott fájlt reprezentálja. (Nem dolgozhatnak ugyanazon a FILE*
-on, mert akkor mindketten megpróbálnák
fclose()
-olni.)
Feladatok
- Írd át úgy az osztályt, hogy ő maga nyissa meg a fájlt is, és konstruktorparamétere legyen a megnyitandó
fájl neve és a megnyitás módja! Tárold el a megnyitott fájl nevét is egy
std::string
-ben! Ha nem sikerült a megnyitás, dobj kivételt!FilePtr fp{"hello.txt", "wt"};
- Oldd meg, hogy lehessen függvényből nyitott fájllal visszatérni!
FilePtr open_for_writing(char const *name) { return FilePtr{name, "wt"}; } FilePtr fp = open_for_writing("hello.txt"); fprintf(fp, "Hello vilag");
- Oldd meg, hogy az így kapott visszatérési értéket az értékadó operátor is
tudja kezelni! Figyelj arra, hogy zárd be a régi fájlt az értékadáskor!
FilePtr fp; fp = open_for_writing("hello.txt"); fprintf(fp, "Hello vilag"); fp = open_for_writing("hello2.txt"); fprintf(fp, "Hello vilag");
- Csinálj tömböt a nyitott fájlokból!
Lehet ilyet? Miért lehet, hogyan van ilyesmire felkészítve azstd::vector<FilePtr> files; files.push_back(open_for_writing("hello.txt")); files.push_back(open_for_writing("hello2.txt")); fprintf(files[0], "hello.txt"); fprintf(files[1], "hello.txt");
std::vector
?
Megoldás
Ehhez mozgató konstruktort és értékadó operátort kell csinálni. Ezek paraméterébe
jobbérték referencia kell, de figyelni kell arra, hogy a jobbérték referencián keresztül
az objektumot balértéknek látjuk (mert neve van). Így std::move
-olni kell
legalább a sztringet, hogy annak a tartalmát is mozgatni lehessen, másolás helyett.
Az std::vector
-nak (és a többi szabványos tárolónak) vannak jobbértéket
átvevő beszúró függvényei, pl. push_back(T&&)
.
#include <cstdio>
#include <vector>
#include <stdexcept>
#include <string>
#include <iostream>
/* FILE* RAII */
class FilePtr {
public:
FilePtr(char const *filename, char const *mode)
: fp_{fopen(filename, mode)}
, name_{filename} {
if (fp_ == nullptr)
throw std::runtime_error("no such file");
std::cerr << "Opened " << name_ << std::endl;
}
~FilePtr() {
close_if_open();
}
/* nem másolható */
FilePtr(FilePtr const &) = delete;
FilePtr & operator=(FilePtr const &) = delete;
/* de mozgatható */
FilePtr(FilePtr && the_other) noexcept
: fp_{std::move(the_other.fp_)}
, name_{std::move(the_other.name_)} {
the_other.fp_ = nullptr;
}
FilePtr & operator=(FilePtr && rhs) noexcept {
if (fp_ != rhs.fp_) {
close_if_open();
/* a pointernél nem lenne muszáj move-olni, de a sztringnél érdemes! */
fp_ = std::move(rhs.fp_);
name_ = std::move(rhs.name_);
rhs.fp_ = nullptr;
}
return *this;
}
operator FILE* () const {
return fp_;
}
private:
FILE *fp_;
std::string name_;
void close_if_open() {
if (fp_ != nullptr) {
fclose(fp_);
std::cerr << "Closed " << name_ << std::endl;
}
fp_ = nullptr;
name_.clear();
}
};
/* ne lehessen ilyet csinálni (az operator FILE* miatt lehetne) */
void fclose(FilePtr) = delete;
FilePtr open_for_writing(char const *name) {
return FilePtr{name, "wt"};
}
int main() {
std::vector<FilePtr> files;
files.push_back(open_for_writing("hello.txt"));
files.push_back(open_for_writing("hello2.txt"));
fprintf(files[0], "hello.txt");
fprintf(files[1], "hello.txt");
}
Egy sztring osztály mozgató konstruktorát kellene megírni. Működik az alábbi programrész? Mi a furcsa benne? (Tipikus hiba.)
class String {
private:
char *data;
public:
String(String &&) noexcept;
};
String::String(String && the_other) noexcept {
data = std::move(the_other.data);
the_other.data = nullptr;
}
Megoldás
A programrész működni fog: a pointer átmásolódik az új objektumba, és a régi objektum pointere
kinullázódik. A furcsaság, vagy inkább hiba, benne az std::move
helytelen használata.
Ebben a kontextusban egyáltalán nincsen rá szükség. Amikor egy sztringtől elvesszük az erőforrást,
akkor a sztring a jobbérték, nem pedig az objektum; a sztringre mondjuk azt, hogy belőle
az erőforrás kivehető. A pointernél már semmi értelme a jobbértékké konvertálásnak; a beépített
típusok amúgy sem mozgathatóak, csak másolhatóak. (Mit jelentene egy int
mozgatása?)
A feladatod egy mátrix osztályt írni, amely a C++11 tanult nyelvi elemeit is használja: jobbérték referenciák, inicializáló listák és így tovább.
Specifikáció, első feladatcsomag
- Az osztály belül ne használjon kétdimenziós dinamikus tömböt, csak egydimenziósat.
Ebbe legyenek sorfolytonosan,
y*szélesség+x
képlettel leképezve a számok!class Matrix { private: size_t w_, h_; double *data_; };
- A mátrixot lehessen inicializálni szélesség, magasság paraméterrel. Ilyenkor legyen kitöltve 0-val.
- Lehessen inicializálni alapértelmezett konstruktorral is, ilyenkor ne foglaljon le erőforrást.
- Lehessen indexelni a függvényhívó operátorral:
m(2, 3)
a 2. oszlop, 3. sor hivatkozása legyen (1-től indexelve). Túlindexelés esetén dobjon kivételt. - Írj a mátrixnak kiíró operátort, amely megjeleníti a képernyőn!
Így már látod majd a mátrixokat, tudod ellenőrizni az eredményeket.
Matrix m{3, 3};
m(1, 1) = 9; m(2, 3) = 5;
std::cout << m;
9 0 0 0 0 0 0 5 0
Konstruktorokkal kapcsolatos feladatok
- Legyen másoló konstruktora, értékadó operátora is a mátrixnak!
- Tegyél kiírást az előbbi függvényekbe, és próbáld ki, az
std::swap()
függvény (#include <algorithm>
) hogyan cserél meg ilyenkor két mátrixot! - Írj mozgató konstruktort és értékadó operátort a mátrixnak!
- Tegyél ezekbe is kiírást, és próbáld ki újra az
std::swap()
függvényt! - Írj két mátrixot összeadó operátort! Ha nem egyforma méretűek, ez dobjon kivételt; ha egyformák, akkor térjen vissza az összegükkel!
Az összeadás optimalizálható, ha az egyik operandus jobbérték. Például az a+b+c
összeadás (a+b)+c
-ként értékelődik ki, tehát a c
egy jobbértékhez
adódik hozzá. Ennek a jobbértéknek ugyanakkora mátrixnak kell lennie, mint a c
.
Ezért a dinamikus memóriakezelést meg lehet spórolni; az újrafoglalás helyett a jobbértékben lévő
tömböt újra lehet hasznosítani az összeg objektum számára.
Az összeadás optimalizálása
- Írd meg ilyen módon a jobbérték+balérték mátrixösszeadást!
- Vezesd ezután vissza a balérték+jobbérték összeadást erre a függvényre! Például ez kell hívódjon az
a+(b+c)
kifejezésnél. Ellenőrizd, hogy tényleg a jobbérték+balérték függvény hívódik-e, nem az eredeti balérték+balérték függvény!Ha az összeadását tagfüggvényként valósítottad meg, nem gond, mert az is túlterhelhető balérték és jobbérték objektumra. Így kell csinálni:
#include <iostream> class X { public: void f() & { std::cout << "lval\n"; } void f() && { std::cout << "rval\n"; } }; int main() { X x; x.f(); /* lval */ X{}.f(); /* rval */ }
- Mi történik ennél a kifejezésnél:
(a+b)+(c+d)
? Oldd meg, hogy ez is működjön!
Az inicializáló listás feladat
- Oldd meg, hogy működjön ez a kódrészlet!
Matrix m{3, 3, { 9, 0, 0, 0, 0, 0, 0, 5, 0 }};
Megoldás
Az összes feladat megoldása. A kommentek magyaráznak, ahol kell. A teszteléshez érdemes kikapcsolni a másolások
optimalizálását (-fno-elide-constructors
), akkor látszik az összes copy és move.
#include <iostream>
#include <iomanip>
#include <initializer_list>
#include <algorithm>
#include <cassert>
/* másolás/mozgatás üzenetek: 0 vagy 1 */
#define DEBUG 1
class Matrix {
public:
/* létrehozás */
Matrix();
Matrix(size_t w, size_t h);
Matrix(size_t w, size_t h, std::initializer_list<double> init);
/* az öt függvény */
~Matrix();
Matrix(Matrix const &the_other);
Matrix & operator=(Matrix const &rhs);
Matrix(Matrix && the_other) noexcept;
Matrix & operator=(Matrix &&rhs) noexcept;
/* hogy csinálni is lehessen vele valamit */
size_t get_w() const { return w_; }
size_t get_h() const { return h_; }
/* konstans indexelés */
double const & operator()(size_t x, size_t y) const;
/* nem konstans indexelés: ugyanaz, mint a konstans, csak a const levarázsolva */
double & operator()(size_t x, size_t y) {
return const_cast<double &>(const_cast<Matrix const &>(*this)(x, y));
}
/* összeadások */
friend Matrix operator+(Matrix const &a, Matrix const &b);
friend Matrix operator+(Matrix &&a, Matrix const &b);
private:
size_t w_;
size_t h_;
double *data_;
class Uninitialized {};
Matrix(size_t w, size_t h, Uninitialized);
};
/* az alapértelmezett konstruktor által létrehozott mátrixnak nincs erőforrása.
* így fog kinézni a mozgató konstruktor által kiürített mátrix is.
* a nullptr-re a delete[] hatástalan, így a destruktorban is jó lesz. */
Matrix::Matrix()
: w_{0}
, h_{0} {
data_ = nullptr;
}
/* ez a konsturktor privát, csak a kódduplikáció elkerülésére van.
* létrehozza a mátrix tömbjét, de nem inicializálja. */
Matrix::Matrix(size_t w, size_t h, Uninitialized)
: w_{w}
, h_{h} {
data_ = new double[w_*h_];
}
/* alapértelmezett konstruktor. nullákkal inicializálja. */
Matrix::Matrix(size_t w, size_t h)
: Matrix{w, h, Uninitialized{}} {
std::fill(data_, data_ + w_*h_, 0.0);
}
/* adott számokkal inicializálás. */
Matrix::Matrix(size_t w, size_t h, std::initializer_list<double> init)
: Matrix{w, h} {
/* ellenőrizzük, hogy tényleg akkora-e az init lista, mint a mátrix.
* ha nem, programozói hiba. azért nem érdemes itt kivételt, mert
* ez nem futási időben derül ki, hanem már a kódban el van rontva. */
assert(w*h == init.size());
std::copy(init.begin(), init.end(), data_);
}
Matrix::~Matrix() {
delete[] data_;
}
/* létrehozza a privát konstruktorral a megfelelő méretű tömböt.
* utána már csak be kell másolnia a számokat. */
Matrix::Matrix(Matrix const &the_other)
: Matrix{the_other.w_, the_other.h_} {
std::copy(the_other.data_, the_other.data_ + w_*h_, data_);
#if DEBUG
std::cerr << w_ << "x" << h_ << " matrix copied\n";
#endif
}
/* op= = dtor+copy */
Matrix & Matrix::operator=(Matrix const &rhs) {
if (this == &rhs)
return *this;
/* ha nem akkora, amekkorából értéket adunk, átméretezés */
if (w_ != rhs.w_ || h_ != rhs.h_) {
delete[] data_;
w_ = rhs.w_;
h_ = rhs.h_;
data_ = new double[w_*h_];
}
/* a számokat másolni mindig kell */
std::copy(rhs.data_, rhs.data_ + w_*h_, data_);
return *this;
}
Matrix::Matrix(Matrix &&the_other) noexcept {
/* ez olyan lesz, mint a másik */
w_ = the_other.w_;
h_ = the_other.h_;
data_ = the_other.data_;
/* a másik meg üres, mint a default ctoros */
the_other.w_ = 0;
the_other.h_ = 0;
the_other.data_ = nullptr;
#if DEBUG
std::cerr << w_ << "x" << h_ << " matrix moved\n";
#endif
}
Matrix & Matrix::operator=(Matrix &&rhs) noexcept {
if (this == &rhs)
return *this;
/* itt nem varázsolunk a méretekkel, mert csak le kell nyúlni a tömböt */
delete[] data_;
w_ = rhs.w_;
h_ = rhs.h_;
data_ = rhs.data_;
rhs.w_ = 0;
rhs.h_ = 0;
rhs.data_ = nullptr;
return *this;
}
/* indexelő operátor. vigyázat, 1-től kell indexelni, mint matek órán! */
double const & Matrix::operator()(size_t x, size_t y) const {
if (x < 1 || x > w_ || y < 1 || y > h_)
throw std::out_of_range{"invalid index for matrix"};
return data_[(y-1)*w_ + (x-1)];
}
std::ostream & operator<<(std::ostream & os, Matrix const &m) {
for (size_t y = 1; y <= m.get_h(); ++y) {
os << "[";
for (size_t x = 1; x <= m.get_w(); ++x)
os << std::setw(4) << m(x, y);
os << " ]\n";
}
return os;
}
/* az összeadásnál nem indexelgetünk. csak egymástól függetlenül
* össze kell adni az összes számot és kész. */
Matrix operator+(Matrix const &a, Matrix const &b) {
if (a.w_ != b.w_ || a.h_ != b.h_)
throw std::out_of_range{"matrix sizes do not match in operator+"};
Matrix sum{a.w_, a.h_, Matrix::Uninitialized{}};
for (size_t i = 0; i != a.w_*a.h_; ++i)
sum.data_[i] = a.data_[i] + b.data_[i];
return sum;
}
/* ha az egyik operandus balérték, akkor lenyúlhatjuk a tömbjét.
* std::move-olni kell, mert a-n keresztül balértéknek látszik!
* (úgy is írhatnánk, hogy a[i]+=b[i], aztán return std::move(a).) */
Matrix operator+(Matrix &&a, Matrix const &b) {
if (a.w_ != b.w_ || a.h_ != b.h_)
throw std::out_of_range{"matrix sizes do not match in operator+"};
Matrix sum{std::move(a)};
for (size_t i = 0; i != a.w_*a.h_; ++i)
sum.data_[i] += b.data_[i];
return sum;
}
/* ha a másik a jobbérték, akkor egyszerűen megcseréljük őket-
* ez oké, mert kommutatív az összeadás. move kell! */
Matrix operator+(Matrix const &a, Matrix &&b) {
return std::move(b)+a;
}
/* ha csak a fenti függvények lennének, jobbérték+jobbérték
* ambiguous lenne. viszont mindegy, melyikből mozgatunk. */
Matrix operator+(Matrix &&a, Matrix &&b) {
return std::move(a)+b;
}
int main() {
Matrix m{2, 3, {
1, 2,
3, 4,
5, 6,
}};
const Matrix n{2, 3, {
2, 3,
4, 5,
6, 7,
}};
std::cout << m;
std::cout << m + n;
std::cout << (m+n)+m; /* jobb+bal */
std::cout << m+(n+m); /* bal+jobb */
std::cout << (m+n)+(n+m); /* jobb+jobb */
}