Objektumok élettartama, okos pointerek
Czirkos Zoltán · 2022.10.11.
Funarg problémák kezelése C++ kódban. RAII és okos pointerek írá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 az első három feladatot meg kell oldani.
Adott az alábbi, Hérón-féle módszerrel gyökvonást végző algoritmus JavaScript kódja. (A módszer lényege: addig finomítjuk a
tippelt gyököt, amíg elég jó nem lesz. A finomítás egy átlagolással végezhető el, mert az igazi gyök a tipp és szám/tipp között
van.) Ez két belső függvényt tartalmaz, amelyek a külső függvény x
változóját is látják. Emlékezz vissza: ez egy ún.
„downwards funarg” probléma. A változó még létezik, de nem a hívott függvény keretében van, hanem fentebb, valamely hívóéban.
function heron(x) {
function good_enough(guess) {
return Math.abs(guess*guess - x) < 0.001;
}
function improve(guess) {
return (guess + x/guess)/2.0;
}
var guess = 1.0;
while (!good_enough(guess))
guess = improve(guess);
return guess;
}
Hogy lehet megcsinálni ugyanezt egy objektummal C++-ban?
int main() {
Heron h;
std::cout << h.get(2.0);
}
Figyelem: nem kell más felépítésű programot írnod, mint a fenti! Szinte ugyanezek a függvények lesznek, szinte csak szintaktikai átalakításról van szó.
Megoldás
A belső függvényekből tagfüggvények lesznek. A minden tagfüggvény által látható változókból pedig tagváltozók.
#include <iostream>
#include <cmath>
class Heron {
bool good_enough(double guess) const {
return std::abs(guess * guess - x_) < 0.001;
}
double improve(double guess) const {
return (guess + x_ / guess) / 2.0;
}
public:
double get(double x) {
x_ = x;
double guess = 1.0;
while (!good_enough(guess))
guess = improve(guess);
return guess;
}
private:
double x_;
};
int main() {
Heron h;
std::cout << h.get(2.0);
}
// Lambdákkal még hasonlóbb megoldás készíthető:
#include <iostream>
#include <cmath>
double heron(double x) {
auto good_enough = [=] (double g) {
return std::abs(g*g - x) < 0.001;
};
auto improve = [=] (double g) {
return (g + x/g)/2.0;
};
double guess = 1.0;
while (!good_enough(guess))
guess = improve(guess);
return guess;
}
int main() {
std::cout << heron(2.0);
}
Adott az alábbi függvény JavaScriptben. Ez paraméterként egy valós→valós
függvényt vesz át. Visszatérési értéke is egy valós→valós függvény, amely az
előbbi numerikus deriváltja (0,001 lépésközű differenciahányadossal közelítve).
Itt egy „upwards funarg” problémát látsz: a visszaadott derivalt()
függvény miatt nem szabadna felszabadítani az f
és dx
lokális változókat.
function derival(f) {
var dx = 0.001;
function derivalt(x) {
return (f(x+dx) - f(x)) / dx;
}
return derivalt;
}
-
Írd meg ugyanezt C++-ban! Egy osztályt kell írnod, amelynek konstruktora egy valós-valós függvényt vesz át, és egy olyan objektumot hoz létre, amelynek van függvényhívó operátora. Ha helyes a derivált, akkor az alábbi program 0-hoz közeli értékeket ír ki, mert
sin()
deriváltjacos()
:#include <iostream> #include <iomanip> #include <cmath> int main() { Derival my_cos = Derival{sin}; for (double f = 0; f < 3.1415; f += 0.1) std::cout << std::setw(20) << my_cos(f)-cos(f) << std::endl; }
Megoldás
A paraméterként kapott függvényt el kell tárolni az objektumban. Ugyanígy kell tenni az összes változóval.
#include <iostream> #include <iomanip> #include <cmath> class Derival { private: using fvtype = double (*)(double); fvtype f_; double dx_; public: Derival(fvtype f): f_{f}, dx_{0.001} { } double operator() (double x) { return (f_(x+dx_) - f_(x)) / dx_; } }; int main() { Derival my_cos = Derival{sin}; for (double f = 0; f < 3.1415; f += 0.1) std::cout << std::setw(20) << my_cos(f)-cos(f) << std::endl; }
-
Mi a helyzet, ha a deriválandó függvény a programkódban nem függvény típusú, hanem valami más? Például ha paraméterezhető parabolát szeretnénk:
f(x)=ax2+bx+c
, ahhoz létrehozható egyParabola
objektum. Ennek szintén a függvényhívó operátora végzi el a számítást:Parabola p1{0.5, 2.3, -5}; /* 0.5x^2 + 2.3x - 5 */ for (double f = 0; f < 3.0; f += 0.1) std::cout << std::setw(20) << p1(f) << std::endl;
Hogy lehet egy ilyet deriválni? Hogyan kell sablonná alakítani a deriváló osztályt? Írd meg a parabolát, és írd át a deriváló osztályt! Az alábbi programnak 0-hoz közeli értékeket kell kiírnia,
p1
numerikusp1_der
és analitikusp2
deriváltjának különbségét:Parabola p1{0.5, 2.3, -5}; /* 0.5x^2 + 2.3x - 5 */ ??????? p1_der = ???????; Parabola p2{0, 1, 2.3}; /* x + 2.3, p1 deriváltja */ for (double f = 0; f < 3.0; f += 0.1) std::cout << std::setw(20) << p1_der(f)-p2(f) << std::endl;
Megoldás
#include <iostream> #include <iomanip> #include <cmath> class Parabola { public: Parabola(double a, double b, double c) : a_{a}, b_{b}, c_{c} { } double operator () (double x) { return a_*x*x + b_*x + c_; } private: double a_, b_, c_; }; template <typename FVTYPE> class Derival { private: FVTYPE f_; double dx_; public: Derival(FVTYPE f): f_{f}, dx_{0.001} { } double operator() (double x) { return (f_(x+dx_) - f_(x)) / dx_; } }; int main() { Parabola p1{0.5, 2.3, -5}; /* 0.5x^2 + 2.3x - 5 */ Derival<Parabola> p1_der = Derival<Parabola>{p1}; Parabola p2{0, 1, 2.3}; /* x + 2.3, p1 deriváltja */ for (double f = 0; f < 3.0; f += 0.1) std::cout << std::setw(20) << p1_der(f)-p2(f) << std::endl; }
Innen letölthető a kifejezéseket kiértékelő és deriváló program: expression.cpp. Emlékezz vissza:
- Ez kifejezéseket (
Expression
), azon belül konstansokat (Constant
), változókat (Variable
), összegeket (Sum
) és szorzatokat (Product
) tudott tárolni. Az utóbbi két osztályt a kód egy közösTwoOperand
ősosztállyal valósítja meg. - A kétoperandusú műveletek lényegében egy kételemű heterogén kollekciót tartalmaznak:
Expression *lhs_, *rhs_
. Ezek erőforráskezelése miatt destruktorra volt szükség. - A szorzat deriválásánál:
(ab)' = a'b + ab'
, a keletkező kifejezésben felhasználjuk a bal és a jobboldali operandusnak nem csak a deriváltját, hanem a másolatát is. Emiatt szükség volt az ismeretlen típusú objektum másolására, amit virtuális konstruktorral lehetett megoldani: ez lett aclone()
függvény.
Feladat:
- Írd át úgy a programot, hogy a nyers pointerek és a kézi erőforráskezelés helyett
std::shared_ptr
-t használjon! Mindenhol, ahol eddig sima pointer volt, ottshared_ptr
-nek kell lennie. - Mely függvények szűnnek meg ezáltal?
- Használd az
std::make_shared<T>(...)
függvényt! Ez dinamikusan lefoglal ésshared_ptr
-be csomagol egyT
típusú objektumot, a...
helyén megadott értékeket a konstruktornak átadva. Pl. az alábbi két sor egyenértékű:return std::shared_ptr<Constant>(new Constant{3.14});
return std::make_shared<Constant>(3.14);
- Miután ez elkészült, gondolkodj el rajta, hogy a szorzat deriválásánál tényleg szükséges-e a másolás,
vagy a részfákat meg lehet-e osztani az egyes kifejezések között. Tehát hogy tényleg szükséges-e a mély
másolat, vagy elegendő a sekély másolat is. Ha elegendő a sekély másolat, akkor a
clone()
teljesen kitörölhető, a megosztást pedig ashared_ptr
kezelni fogja. - Vannak olyan kommentek a kódban, amelyeket ki kellene törölni?
Megoldás
A TwoOperand
erőforráskezelő függvényei (destruktor, másoló konstruktor) teljesen eltűnnek.
A kifejezésfák megoszthatók. A sekély másolat azért engedhető meg, mert a kifejezések soha nem módosulnak,
az összes objektum értéket jelképez, konstans. Ha lenne pl. Constant::set_value()
, vagy
Sum::set_left_operand()
függvény, akkor ez nem lenne igaz!
A dinamikus memóriakezeléssel kapcsolatos kommenteket törölni kell – a shared_ptr
-ek segítségével
automatikussá vált a memóriakezelés, így teljesen lényegtelen, hogy a háttérben dinamikus memóriakezelés van.
Észre kell venni: az átalakított kódban sehol sem szerepel sem a new
, sem a delete
szó!
A teljes megoldás: expression_shared.cpp.
FILE*
a C++-ban is van, a cstdio
fejlécfájlt kell
használni hozzá. A szokásos függvényei is megvannak, fopen()
,
fclose()
, fprintf()
, tökéletesen ugyanaz minden, mint
C-ben.
A megnyitott fájl egy olyan erőforrás, amit nem a delete
operátorral kell felszabadítani, így a beépített okos pointerek nem jók hozzá. A
feladatod egy olyan osztályt írni, amely RAII becsomagol egy FILE*
pointert. Az objektumot át kell tudni adni a szokásos C-s függvényeknek. Az
alábbi főprogramnak kell működnie, és a fájlnak automatikusan be kell záródnia:
int main() {
FilePtr fp;
fp = fopen("hello.txt", "wt");
fprintf(fp, "Hello vilag");
}
Milyen operátort kell írni az osztálynak ahhoz, hogy az
fopen()
-es sor működjön? Figyeld az értékadás két oldalán álló típusokat!Milyen operátor kell ahhoz, hogy az
fprintf()
-es sor működjön?FilePtr
objektumokat nem szabad lemásolni és értékül adni, mert akkor a bennük tároltFILE*
átmásolódna az új objektumba, és kétszer lenne bezárva egy fájl. Tiltsd le a másoló konstruktort és az értékadó másolás operátort!Aki nem ismeri ezt az osztályod pontosan, és nem tudja, hogy az
fclose()
automatikusan meghívódik a destruktorból, esetleg megpróbálhatná kézzel meghívni a függvényt:fclose(fp)
. Márpedig ha azfprintf()
-es sor lefordul, akkor ez is, és kétszer záródik be a fájl. Meg lehet valahogyan oldani, hogy ilyet ne engedjen a fordító?
Megoldás
Az fclose()
-hoz: Kell lennie egy olyan fclose()
nevű
függvénynek, amely konverzió nélkül is át tud venni egy FilePtr
-t.
Ha van ilyen, akkor az fclose(fp)
-nél azt fogja használni a fordító.
A konverzió nélküli értelmezésnek mindig elsőbbsége van. Megírni a függvényt nem
kell, legjobb = delete
deklarálni.
#include <cstdio>
class FilePtr {
public:
explicit FilePtr(FILE *fp = nullptr) : fp_{fp} {}
~FilePtr() {
close_if_open();
}
FilePtr(FilePtr const &) = delete;
FilePtr & operator=(FilePtr const &) = delete;
FilePtr & operator=(FILE *fp) {
close_if_open();
fp_ = fp;
return *this;
}
operator FILE* () const {
return fp_;
}
private:
FILE *fp_;
void close_if_open() {
if (fp_ != nullptr)
fclose(fp_);
fp_ = nullptr;
}
};
void fclose(FilePtr) = delete;
int main() {
FilePtr fp;
fp = fopen("hello.txt", "wt");
fprintf(fp, "Hello vilag");
}