Osztályonkénti operator new
Egy C++ program rengeteg objektumot hoz létre és töröl ki a futása közben. Ezek sokszor néhány fontos osztály objektumai, pl. láncolt listák elemei, üzenetek, pontok, vonalak és hasonlók. Sok apró objektumnál a foglalás és felszabadítás nem túl hatékony, sőt a dinamikusan kezelt memóriaterület töredezettségét is okozza. Ez azt jelenti, hogy a lefoglalt területek nem egybefüggőek; rossz esetben a közöttük lévő üres hely túl kicsi ahhoz, hogy ott új objektum, főleg tömb, elférjen.
A problémán többféleképp lehet segíteni. Az egyik jól bevált megoldás az, ha a különféle osztályoknak saját new
és delete
operátort írunk. Az eltérő típusú objektumok így különálló „készletből” kaphatnak memóriaterületet
(memory pool), és a foglalás is gyorsabb lehet, mivel az egyes készletek nyilvántartása egymástól független lehet. Az
osztályspecifikus dinamikus memóriakezeléshez nem kell mást tenni, mint a globális new
és delete
operátorokhoz hasonló fejlécű tagfüggvényeket definiálni az osztályban. Ha létezik T::operator new
, akkor a fordító
egy new T
kifejezésnél automatikusan ezt használja a globális new
helyett.
Az osztálybeli operator new
a szokásos módon
átveszi paraméterként a méretet: gondoljunk arra, hogy leszármazás esetén a
leszármazott objektum más méretű lehet. A tömbökhöz való new[]
és
delete[]
operátort külön kell átdefiniálni.
Tegyük fel, hogy egy programban a Gadget
típusú objektumok száma
nagyon gyorsan változik, ugyanakkor tudjuk, hogy maximum 64 darab lesz belőlük. A
feladatod az osztályhoz egy olyan operator new
és
delete
párost írni, amelyik egy alkalmasan méretezett
memóriaterületről foglalja a Gadget
objektumokat! Ez most az
egyszerűség kedvéért legyen majd egy 64*sizeof(Gadget)
bájt méretű
tömb az objektumoknak, és egy bool[64]
tömb, amely a foglaltságokat
mutatja.
Az igazi, gyors memóriakezelők nem pont így működnek: a foglaltság tömbjében lineáris keresést végezni nyilvánvalóan lassú művelet. Ez a feladat a technika használatát és a szintaxist hivatott bemutatni.
- Írd meg a
Gadget
osztályt egy, hogy legyen egyint
adattagja, amely beállítható, lekérdezhető! Teszteld előbb szokványos dinamikus memóriakezeléssel. - Hozz létre egy
GadgetAllocator
osztályt, amelynek statikus változója az objektumokhoz való tömb és a foglaltságok tömbje. Írj ennek az osztálynak egy statikusallocate()
ésfree()
tagfüggvényt, amelyek anew
ésdelete
operátorhoz hasonló fejléccel és szereppel rendelkeznek! (Ha aGadget
osztály barát, akkor ezek akár privátak is lehetnek.) Figyelj arra, hogy belül indexekkel dolgozol, de a hívónak pointert kell adnod, és pointert fogsz kapni. Használd areinterpret_cast
-ot a pointerek konverziójához! - Írd meg a
Gadget
osztálynew
ésdelete
operátorát úgy, hogy aGadgetAllocator
allocate()
ésfree()
függvényeit használják! - Teszteld így is az osztályt!
- Adj hozzá egy statikus függvényt, amely a
Gadget
-ek memóriakészletének foglaltsági térképét rajzolja ki! Ellenőrizd ezzel is a működést! - Ha még nem dob kivételt az
operator new
, alakítsd át: ha nincs többGadget
-nek hely, dobjonstd::bad_alloc
kivételt. - Észreveheted, hogy azt is meg tudod mondani az
operator delete
függvényben, ha nem foglalt objektumot próbál a hívó felszabadítani. Dobj ekkor is kivételt! - A foglaltsági táblázatból bármikor lehet tudni, hol van élő objektum a tömbben. Írj egy függvényt, amely varázsütésre felszabadítja az összeset! Milyen hasznos dolgot tud ez csinálni azon kívül, hogy szabad jelöléseket tesz a foglaltság tömbbe?
- Mi történik, ha valaki leszármazik a
Gadget
osztályból, és egy leszármazottat foglal dinamikusan? Tegyél valamit ezellen!
Megoldás
A memóriát char[]
tömbből kell építeni, de Gadget
objektumokat
fog tartalmazni, és azokra mutató pointerekkel kell dolgozni. Ezért kell reinterpret_cast<Gadget*>
-ot használni, amikor a memóriaterületen a
Gadget
-ek címeit ki kell számolni.
Az összes Gadget
-et felszabadító függvény meghívhatja a Gadget
-ek
destruktorait is.
A Gadget
osztály ha final, nem lehet már leszármazni belőle. Így nincs
olyan veszély, hogy egy nagyobb méretű leszármazottal kel foglalkoznia az allocate()
függvénynek.
A két osztály deklarációi és definíciói egymásra hivatkoznak; a Gadget::operator new
előre hivatkozik a GadgetAllocator::allocate()
függvényre, a GadgetAllocator
osztály definíciója pedig a sizeof(Gadget)
értékére. Ezért nem tudjuk osztályon belül
definiálni a függvényeket.
#include <new>
#include <iostream>
#include <stdexcept>
#include <vector>
#define DEBUG 0
//
// Sample object which will be allocated from a memory pool
//
class Gadget final {
public:
Gadget(int i) : i_(i) { std::cout << "Gadget " << i_ << " ctor\n"; }
~Gadget() { std::cout << "Gadget " << i_ << " dtor\n"; }
void * operator new(size_t size);
void operator delete(void *mem) noexcept;
private:
int i_;
};
//
// Memory pool for Gadget objects
//
class GadgetAllocator {
public:
friend class Gadget;
static void show_allocation_map();
static void delete_all();
private:
static void *allocate();
static void free(void *mem);
static constexpr size_t poolsize_ = 64;
static char pool_[sizeof(Gadget) * poolsize_];
static bool allocated_[poolsize_];
};
char GadgetAllocator::pool_[sizeof(Gadget) * poolsize_];
bool GadgetAllocator::allocated_[poolsize_];
void *GadgetAllocator::allocate() {
/* find first unallocated */
size_t i;
for (i = 0; i != poolsize_; ++i)
if (allocated_[i] == false)
break;
/* if did not break, no more memory left */
if (i == poolsize_) {
#if DEBUG
std::cerr << "Cannot allocate Gadget" << std::endl;
#endif
throw std::bad_alloc{};
}
/* set allocated flag */
#if DEBUG
std::cerr << "Allocating Gadget at " << i << std::endl;
#endif
allocated_[i] = true;
return static_cast<void*>(reinterpret_cast<Gadget*>(pool_) + i);
}
void GadgetAllocator::free(void *mem) {
/* calculate index */
size_t i = static_cast<Gadget*>(mem) - reinterpret_cast<Gadget*>(pool_);
/* check if proper pointer and allocated */
if (i >= poolsize_ || !allocated_[i]) {
#if DEBUG
std::cerr << "Cannot deallocate " << mem << std::endl;
#endif
throw std::bad_alloc{};
}
/* reset allocated flag */
#if DEBUG
std::cerr << "Freeing Gadget at " << i << std::endl;
#endif
allocated_[i] = false;
}
void GadgetAllocator::show_allocation_map() {
for (size_t i = 0; i != poolsize_; ++i) {
std::cout << allocated_[i];
if ((i+1) % 16 == 0)
std::cout << std::endl;
}
}
void GadgetAllocator::delete_all() {
for (size_t i = 0; i != poolsize_; ++i) {
if (allocated_[i]) {
reinterpret_cast<Gadget*>(pool_)[i].~Gadget();
allocated_[i] = false;
}
}
}
// Allocator and deallocator for Gadgets - calls GadgetAllocator's functions
void * Gadget::operator new(size_t size) {
return GadgetAllocator::allocate();
}
void Gadget::operator delete(void *mem) noexcept {
return GadgetAllocator::free(mem);
}
int main() {
std::vector<Gadget*> my_gadgets;
std::cout << "Allocate 10 gadgets:" << std::endl;
for (int i = 0; i < 10; ++i)
my_gadgets.push_back(new Gadget{i});
GadgetAllocator::show_allocation_map();
std::cout << "Delete 5 random gadgets:" << std::endl;
for (int i = 0; i < 5; ++i) {
int del = rand() % my_gadgets.size();
delete my_gadgets[del];
my_gadgets[del] = my_gadgets.back();
my_gadgets.pop_back();
}
GadgetAllocator::show_allocation_map();
std::cout << "2 new gadgets to empty slots:" << std::endl;
my_gadgets.push_back(new Gadget{11});
my_gadgets.push_back(new Gadget{12});
GadgetAllocator::show_allocation_map();
std::cout << "Use up all remaining slots:" << std::endl;
try {
while (true)
my_gadgets.push_back(new Gadget{rand() % 100});
} catch (std::bad_alloc &) {
}
GadgetAllocator::show_allocation_map();
std::cout << "Delete all gadgets not deleted so far:" << std::endl;
GadgetAllocator::delete_all();
GadgetAllocator::show_allocation_map();
}
Allokátor objektumok
Az előbbi GadgetAllocator
osztálynak minden adattagja statikus
volt. Ez azt jelenti, hogy az egész programban egyetlen
GadgetAllocator
létezett.
Ennek nem feltétlenül kell így lennie. Sőt nagyon jó ötlet megcsinálni azt, hogy
több allokátor legyen. Képzeljük el az alábbi szituációt. Szükségünk van egy csapat
Gadget
objektumra. Néha foglalunk egy csomót, néha felszabadítunk párat,
aztán egyszercsak már nincsen szükség a csapat egyik tagjára sem. Ebben a helyzetben
a következőt tennénk: létrehozunk egy GadgetAllocator
objektumot. Az
egyes Gadget
-eket ebben hozzuk létre. Aztán amikor már nincs szükség
az objektumokra, csak az allokátort kell kitörölni, mert annak destruktora kitörlheti
a megmaradt Gadget
-eket.
Mindezt a „placement new” szintaxis lehetővé teszi. Ugyanis az nem csak arra jó,
hogy adott helyen hozzunk létre objektumot, hanem arra is, hogy egy osztály
saját operator new
-jának további paramétereket adjunk át. Az első
paraméter ezeknél az operator new
függvényeknél mindig a méret, utána
pedig az összes többi paramétert megkapják:
class Gadget {
void *operator new(size_t size); /* 1 */
void *operator new(size_t size, int a, char b); /* 2 */
};
Gadget *g1 = new Gadget{}; /* 1, Gadget::operator new(sizeof(Gadget)) */
Gadget *g2 = new (5, 'b') Gadget{}; /* 2, Gadget::operator new(sizeof(Gadget), 5, 'b'); */
Tehát az operator new
függvény ilyen módon túlterhelhető: az első
new
a felső, kvázi paraméter nélküli változatot hívja, az alsó
new
pedig az int a, char b
paraméterekkel rendelkezőt.
Így könnyen megcsinálhatjuk az allokátort! A new
-nak egyszerűen
„placement new” szintaxissal meg kell majd adni, hogy melyik
GadgetAllocator
objektumtól kérje a memóriaterületet. Még azt is
megtehetjük, hogy az allokátor nélküli esetben a beépített memóriakezelést,
globális ::operator new
-t használjuk. Így a Gadget
objektumok
használója azt is eldöntheti, hogy szüksége van-e az allokátorok többlet szolgáltatásaira,
vagy nincs.
A delete
operátoroknál viszont nincs ilyen túlterhelési lehetőség, abból
csak egyféle van. Emiatt valahogyan egy delete
operátorban tudni kell, hogy
az a Gadget
honnan lett foglalva: melyik allokátorból (vagy esetleg a
közös memóriaterületről). Ezt úgy lehet legegyszerűbben megoldani, hogy a Gadget
objektumokba beteszünk egy pointert, amelyik az allokátorra mutat, amelyik őt
foglalta (vagy null pointer, ha a közös memóriaterületen van). Az egyes függvények tehát:
- A „paraméter nélküli”
new
: a globális memóriaterületről foglal egyGadget
-nek való helyet, és null pointert ír az allokátorhoz. - A „paraméteres”
new
lát egy allokátort, és attól kér memóriaterületet. - A
delete
operátor pedig belenéz az objektumba: ha null pointer van megadva allokátornak, akkor a globálisdelete
operátorral törlendő a memóriaterület, ha nem null, akkor annak az allokátornak afree()
-jét kell hívni.
A Gadget
osztály rendes tagfüggvényei nem szabad ehhez a pointerhez
nyúljanak. (A szép megoldás erre az lenne, ha egy ősből származtatjuk, ahol az
ősnek privát adattagja ez a pointer.) Az érdekesség, hogy az operator new
-ok
olyan Gadget
objektumban lévő pointer értékét állítják be, amely még
elvileg nem létezik, mert a konstruktora nem futott le. De ez nem gond, mert a memóriaterületet
már lefoglalják, és amikor az le van foglalva, onnantól írni is szabad oda. Ugyanez
a helyzet a Gadget
operator delete
-jével: miután lefutott
a konstruktor, csak akkor jön az operator delete
! Így az a már nem létező
objektum pointerét olvassa ki. De ez sem gond, mert bár a destruktor már lefutott,
a memóriaterület még nem lett felszabadítva, azt épp a delete
operátor
fogja megtenni.
Implementáld az itt elmondottakat!
Ez a feladat is az ötletet hivatott bemutatni.
- Tegyél egy
GadgetAllocator *
pointert aGadget
osztályba. - Írd át a
GadgetAllocator
osztályt, hogy semmi se legyen benne statikus. Írj neki konstruktort és destruktort: a foglaltságot jelző tömböt inicializálni kell. - Deklaráld és definiáld a kétféle
operator new
függvényt. Az egyik a globális memóriaterületről kell foglaljon (::operator new
), a másik a paraméterként kapott allokátortól kér memóriaterületet. - Írd át az
operator delete
-et is. Ennek figyelnie kell az objektumban lévő, allokátorra hivatkozó pointert. A törlendő objektumot ezvoid *
-ként kapja, de ez bátran átalakíthatóGadget *
-gá, mert egyGadget
helyére mutat. - Írd át a főprogramot, hogy hozzon az létre egy
GadgetAllocator
objektumot. Mindenhol add át ezt az objektumot anew
kifejezésekben:GadgetAllocator a1; Gadget *g1 = new (a1) Gadget{5};
Megoldás
#include <new>
#include <iostream>
#include <stdexcept>
#include <vector>
#define DEBUG 0
class GadgetAllocator;
class Gadget final {
public:
Gadget(int i) : i_(i) { std::cout << "Gadget " << i_ << " ctor\n"; }
~Gadget() { std::cout << "Gadget " << i_ << " dtor\n"; }
void * operator new(size_t size);
void * operator new(size_t size, GadgetAllocator &allocator);
void operator delete(void *mem) noexcept;
private:
int i_;
GadgetAllocator *allocator_;
};
//
// Memory pool for Gadget objects
//
class GadgetAllocator {
public:
GadgetAllocator() : allocated_{} {}
GadgetAllocator(GadgetAllocator const &) = delete;
GadgetAllocator& operator=(GadgetAllocator const &) = delete;
~GadgetAllocator() { delete_all(); }
void show_allocation_map();
void delete_all();
friend class Gadget;
private:
void *allocate();
void free(void *mem);
static constexpr size_t poolsize_ = 64;
char pool_[sizeof(Gadget) * poolsize_];
bool allocated_[poolsize_];
};
void GadgetAllocator::show_allocation_map() {
for (size_t i = 0; i != poolsize_; ++i) {
std::cout << allocated_[i];
if ((i+1) % 16 == 0)
std::cout << std::endl;
}
}
void *GadgetAllocator::allocate() {
/* find first unallocated */
size_t i;
for (i = 0; i != poolsize_; ++i)
if (allocated_[i] == false)
break;
/* if did not break, no more memory left */
if (i == poolsize_) {
#if DEBUG
std::cerr << "Cannot allocate Gadget" << std::endl;
#endif
throw std::bad_alloc{};
}
/* set allocated flag */
#if DEBUG
std::cerr << "Allocating Gadget at " << i << std::endl;
#endif
allocated_[i] = true;
return static_cast<void*>(reinterpret_cast<Gadget*>(pool_) + i);
}
void GadgetAllocator::free(void *mem) {
/* calculate index */
size_t i = static_cast<Gadget*>(mem) - reinterpret_cast<Gadget*>(pool_);
/* check if proper pointer and allocated */
if (i >= poolsize_ || !allocated_[i]) {
#if DEBUG
std::cerr << "Cannot deallocate " << mem << std::endl;
#endif
throw std::bad_alloc{};
}
/* reset allocated flag */
#if DEBUG
std::cerr << "Freeing Gadget at " << i << std::endl;
#endif
allocated_[i] = false;
}
void GadgetAllocator::delete_all() {
for (size_t i = 0; i != poolsize_; ++i) {
if (allocated_[i]) {
reinterpret_cast<Gadget*>(pool_)[i].~Gadget();
allocated_[i] = false;
}
}
}
// Allocator and deallocator for Gadgets - calls GadgetAllocator's functions
// Default operator new: allocate from free store
void * Gadget::operator new(size_t size) {
void *mem = ::operator new(size);
Gadget *constructed_gadget = static_cast<Gadget *>(mem);
constructed_gadget->allocator_ = nullptr;
return mem;
}
// Allocate from GadgetAllocator
void * Gadget::operator new(size_t size, GadgetAllocator &allocator) {
void *mem = allocator.allocate();
Gadget *constructed_gadget = static_cast<Gadget *>(mem);
constructed_gadget->allocator_ = &allocator;
return mem;
}
// Deallocate Gadget. If allocator is null, delete from free store,
// if not null, ask allocator to deallocate.
void Gadget::operator delete(void *mem) noexcept {
Gadget const *deleted_gadget = static_cast<Gadget const *>(mem);
if (deleted_gadget->allocator_ == nullptr)
::operator delete(mem);
else
deleted_gadget->allocator_->free(mem);
}
int main() {
{
GadgetAllocator a1;
std::vector<Gadget*> my_gadgets;
std::cout << "Allocate 10 gadgets:" << std::endl;
for (int i = 0; i < 10; ++i)
my_gadgets.push_back(new (a1) Gadget{i});
a1.show_allocation_map();
std::cout << "Delete 5 random gadgets:" << std::endl;
for (int i = 0; i < 5; ++i) {
int del = rand() % my_gadgets.size();
delete my_gadgets[del];
my_gadgets[del] = my_gadgets.back();
my_gadgets.pop_back();
}
a1.show_allocation_map();
std::cout << "2 new gadgets to empty slots:" << std::endl;
my_gadgets.push_back(new (a1) Gadget{11});
my_gadgets.push_back(new (a1) Gadget{12});
a1.show_allocation_map();
std::cout << "Use up all remaining slots:" << std::endl;
try {
while (true)
my_gadgets.push_back(new (a1) Gadget{rand() % 100});
} catch (std::bad_alloc &) {
}
a1.show_allocation_map();
std::cout << "Exiting code block, allocator will be destructed:" << std::endl;
}
std::cout << "Allocator destructed." << std::endl;
}
decltype
„Írjunk függvénysablont, amely átvesz két iterátort, amelyek egy tartományt határoznak meg, és visszatér a tartományban tárolt legkisebb értékkel! Ha a tartomány üres, akkor a függvénynek kivételt kell dobnia.” Ennek a feladatnak az alábbi kód szeretne a megoldása lenni:
template <typename ITER>
auto find_min(ITER begin, ITER end) -> decltype(*begin) {
if (begin == end)
throw "ures";
auto min = *begin;
while (++begin != end)
if (*begin < min)
min = *begin;
return min;
}
A megvalósítás azonban hibás. Miért? Hogyan lehetne javítani? Ha nincs ötleted, nézd meg, mit szól hozzá a fordító!
Auto i{3}
Mi a típusa auto i{3};
definíció esetén az i
változónak? És az
auto i = {3};
esetben? Miért?
Megoldás
Az előbbi int
, az utóbbi viszont std::initializer_list<int>
.
Az egy elemű kapcsos zárójelre külön szabályok vonatkoznak, épp emiatt. Eredetileg ez el is
volt rontva a C++11 szabványban, később javították.
ReverseView
A „range based for” ciklus be tud járni bármilyen tárolót. De csak előrefelé, pedig a legtöbb szabványos tárolónak
van reverse_iterator
-a is. Ezt az rbegin()
, rend()
tagfüggvényekkel kapjuk meg,
és amellyel bár látszólag előrefelé megyünk (it++
), az elemeket mégis fordított sorrendben kapjuk.
Írj adaptert, amely segítségével a „range based for” ciklusban fordított sorrendben látjuk az elemeket! Ügyelj arra, hogy ez nem új tároló létrehozását jelenti, tehát nem másolhatók le az elemek!
std::vector<int> vec = { 1, 2, 3 };
for (auto i : make_reverse_view(vec))
std::cout << i; // 3 2 1
std::list<int> const lis = { 1, 2, 3 };
for (auto i : make_reverse_view(lis))
std::cout << i;
int arr[3] = { 1, 2, 3 };
for (auto i : make_reverse_view(arr))
std::cout << i;
Megoldás
#include <vector>
#include <list>
#include <iostream>
template <typename C>
class ReverseView {
private:
C & container;
public:
ReverseView(C & container) : container(container) {}
auto begin() const {
return std::rbegin(container);
}
auto end() const {
return std::rend(container);
}
auto rbegin() const {
return std::begin(container);
}
auto rend() const {
return std::end(container);
}
};
template <typename C>
auto make_reverse_view(C & container) {
return ReverseView<C>(container);
}
int main() {
std::vector<int> vec = { 1, 2, 3 };
for (auto i : make_reverse_view(vec))
std::cout << i; // 3 2 1
std::list<int> const lis = { 1, 2, 3 };
for (auto i : make_reverse_view(lis))
std::cout << i;
int arr[3] = { 1, 2, 3 };
for (auto i : make_reverse_view(arr))
std::cout << i;
}
A megoldás C++14-es, és igényel némi magyarázatot.
A tároló tagfüggvényeinek hívása lényegében triviális; a view.begin()
a tároló rbegin()
függvényét, a view.rbegin()
a tároló begin()
függvényét hívja meg, így oda-vissza megfordul a sorrend.
A megoldás konstans tároló esetén is működik. Ilyen esetben ugyanis a
make_reverse_view()
függvénysablon képes példányosodni: az std::list<int> const
listát paraméterként átadva T = std::list<int> const
lesz a sablonparaméter, ezért
a függvény paramétere konstans referencia, és a ReverseView
objektum is konstans
referenciát fog tárolni. Az abból meghívott begin/end
, illetve rbegin/rend
tagfüggvényeknek pedig szokott lenni const
overloadja (!), ami a tároló const_iterator
belső típusát adja (vagy const_reverse_iterator
-t).
Így egyébként a konstans és nem konstans tároló esetén a tagfüggvények auto
visszatérési
értékei is hol konstans, hol nem konstans iterátorrá vezetődnek le. Ezeket C++98-ban nagyon körülményes
lett volna megadni.
Tömbre azért működik a dolog, mert az std::begin()
és std::rbegin()
és párjaik
működnek tömbre is (tömb referenciáját veszik át paraméterként). Tömbökre az std::begin()
talán pointert ad (az elvégre is iterátorként használható). Az std::rbegin()
pedig bizonyára
valamilyen segédosztályból példányosított iterátort, mert ennek a ++
operátora visszafelé kell
lépkedjen (--
), de ez pointerrel nem lenne megoldható.
Legjobb lenne a make_reverse_view
függvénynél C &&
referenciát használni,
de erről a tananyag későbbi részében lesz csak szó.
Python range()
A Python nyelvben könnyen lehet egy számtartományon iterálni:
for i in range(0, 5):
print(i)
0 1 2 3 4
A for ... in
egy tároló bejárására való, ahogyan a C++ range based for nyelvi eleme is. A range()
függvény nagy okossága, hogy egy ún. generátor objektumot hoz létre. Vagyis nem épít fel egy tárolót, benne az összes számmal,
hanem igény szerint állítja elő mindig a számsor következő elemét.
Ilyesmi a C++-ban is megvalósítható: egy olyan osztályt kell írni hozzá, amelynek iterátorai adják a számokat. Írj egy ilyet!
for (auto i : range(0, 5)) // 0 1 2 3 4
std::cout << i << " ";
for (auto i : range(5, 0, -1)) // 5 4 3 2 1
std::cout << i << " ";
for (auto i : range(5)) // 0 1 2 3 4
std::cout << i << " ";
for (auto i : range(1, 10, 2)) // 1 3 5 7 9
std::cout << i << " ";
try {
range(1, 10, 0); // a differencia nem lehet 0
} catch (std::exception & e) {
std::cerr << e.what();
}
Iterátor adapterek
A range-based for ciklussal könnyű iterálni egy tárolón:
std::list<std::string> szavak = {"alma", "körte", "barack"};
for (auto s : szavak)
std::cout << s << std::endl;
A gond csak az, hogy egy ilyen iterálás esetén indexeket nem látunk. Az s
változóban a sztringet megkapjuk, de
hogy melyik sztring hányadik volt, arról nincs információnk. Hacsak nem hozunk létre és növelgetünk a ciklusban egy külön változót
erre a célra, de az elrontja a nyelvi elem szépségét, egyszerűségét.
A megoldást egy iterátor adapter adja, amelyik „összecsomagolja” a tárolóból vett értéket és a hozzá tartozó indexet. Pythonban
is van ilyen, az enumerate()
függvény használata esetén tuple
-kben kapjuk meg az összetartozó
index–érték párokat:
szavak = ["alma", "körte", "barack"]
for index, ertek in enumerate(szavak):
print(index, ertek)
0 alma 1 körte 2 barack
De ugyanez működik C++-ban, a Boost Ranges könyvtár segítségével is:
using namespace boost::adaptors;
for (auto const& v : szavak | indexed(0))
std::cout << v.index() << " "
<< v.value() << std::endl;
0 alma 1 körte 2 barack
Ha szeretnénk, itt még azt is megadhatjuk az indexed()
függvény paraméterével, hogy 1-től induljon
a szavak számozása; az adapter által előállított indexeknek nem feltétlenül kell megegyezniük a tárolóbeli indexekkel,
amik mellesleg a lista esetén nem is léteznek.
A feladat: írj egy ilyen iterátor adaptert! Az adaptert választásod szerint lehessen létrehozni operátorral (mint
a Boost-os példában a |
operátor), vagy függvénnyel (enumerate()
a Pythonos példában). Az elemeket
és indexeket lehessen elérni a Boost stílusában, a .value()
és .index()
függvényekkel, vagy a Python
stílusában tedd őket egy párba (std::pair
). Az utóbbi esetben kicsit egyszerűbb megoldáshoz jutsz majd, a használat
pedig ilyen lesz:
std::list<std::string> szavak = {"alma", "körte", "barack"};
for (auto p : enumerate(szavak, 0))
std::cout << p.first << " " << p.second << std::endl;
Megoldás
Python enumerate()
-hez hasonló létrehozás, és az iterálás (index; érték) párokat ad:
#include <functional>
#include <iostream>
#include <list>
template <typename T>
class Enumerator {
private:
T & container;
int startidx;
public:
/* megjegyzi, melyik tárolóról van szó, és hányadik indexről */
Enumerator(T & container, int startidx) : container(container), startidx(startidx) {}
/* becsomagolja az iterátort, hozzá tartozóan egy indexszel */
class Iterator {
private:
using Iterator_of_T = decltype(std::begin(container));
Iterator_of_T it; // iterator_of<container>
int idx;
public:
Iterator(Iterator_of_T it, int idx = 0) : it(it), idx(idx) {}
/* minimál iterátor interfész, hogy működhessen a range-based for:
* léptetés, egyenlőségvizsgálat és dereferálás */
Iterator & operator++() {
++it;
++idx;
return *this;
}
bool operator==(Iterator const & rhs) const {
return it == rhs.it;
}
bool operator!=(Iterator const & rhs) const {
return it != rhs.it;
}
/* ez főként ez határozza meg az interfészt a range-based for-ban.
* egy párt ad vissza, .first és .second érik majd el az indexet és az adatot.
* *it valószínűleg egy referencia, ezért a párban is referencia lesz. */
auto operator*() const {
return std::pair<int, decltype(*it)>(idx, *it);
}
};
Iterator begin() {
return Iterator(std::begin(container), startidx);
}
Iterator end() {
return Iterator(std::end(container));
}
};
template <typename T>
auto enumerate(T & container, int startidx) {
return Enumerator<T>(container, startidx);
}
int main() {
std::list<std::string> szavak = {"alma", "körte", "barack"};
for (auto p : enumerate(szavak, 1))
std::cout << p.first << " " << p.second << std::endl;
}
Ugyanez .index()
és .value()
-képes segédobjektumokkal:
template <typename T>
class Enumerator {
/* ... */
class Iterator {
/* ... */
auto operator*() const {
/* a pár helyett egy segédosztály */
return ValueHelper(*this);
}
};
/* a segédosztály csak eltárolja a fenti iterátort, hogy aztán abból
* kiolvashassa az elemet és az indexet */
class ValueHelper {
private:
Iterator it;
public:
ValueHelper(Iterator it) : it(it) {}
decltype(auto) value() const {
return *it.it;
}
int index() const {
return it.idx;
}
};
};