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.

1. Másolások optimalizálása

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.

2. Tulajdonos szemantika: a fájl

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!
    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");
    Lehet ilyet? Miért lehet, hogyan van ilyesmire felkészítve az 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");
}

3. std::move

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?)

4. Hatékonyság: a mátrix

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 */
}

5. További feladatok