Při revizi kódu se pravidelně setkávám s následující chybou, kdy dochází k uchovávání referencí na dočasné objekty. Problém se vyskytuje nejen v C++, ale i C. Já jej ovšem budu ilustrovat v C++.
(De)motivační příklad č. 1 (std::string)
Mějme následující kód:
std::string x_n_times(std::size_t n) { return std::string(n, 'x'); } void needs_c_strings(const char* cs1, const char* cs2) { std::cerr << std::strlen(cs1) + std::strlen(cs2) << '\n'; } int main() { // Takto ne! auto cs1 = x_n_times(2).c_str(); auto cs2 = x_n_times(3).c_str(); needs_c_strings(cs1, cs2); }
V kódu je funkce, která vrací std::string
. Její implementace pro nás není v tuto chvíli podstatná, ale vězte, že nám vrátí řetězec obsahující takový počet písmen x
, kolik jsme jí předali při volání. Jelikož po jejím zavolání potřebujeme zavolat funkci, která jako parametry očekává Céčkové řetězce (const char*
), tak je potřeba na std::string
řetězcích zavolat metodu c_str()
. Ve funkci pak pouze vytiskneme součet délek obou řetězců.
Když si kód přeložíme a spustíme, tak nám buď spadne, nebo vypíše nekorektní výsledek (6 místo 5). Proč? Zkuste popřemýšlet a poté čtěte dál.
Proč program spadne?
Pojďme se nejdříve podívat na situaci, kdy nám program spadne. Když si jej spustíme přes valgrind
, tak dostaneme takovéto varování:
Invalid read of size 1 at 0x4C30122: strlen by 0x108CF4: needs_c_strings(char const*, char const*) by 0x108DA1: main Address 0x5aecc80 is 0 bytes inside a block of size 101 free'd at 0x4C2E64B: operator delete(void*) by 0x108D8E: main Block was alloc'd at at 0x4C2D52F: operator new(unsigned long) by 0x4F5DACC: std::__cxx11::basic_string<...>::_M_construct(unsigned long, char) by 0x108C81: x_n_times[abi:cxx11](unsigned long) by 0x108D72: main
Z něj lze vyčíst, že při volání strlen()
čteme paměť, která již byla uvolněná. Následující řádek
auto cs1 = x_n_times(2).c_str();
totiž znamená toto:
- Zavolej
x_n_times(2)
. - Na vráceném
std::string
zavolejc_str()
, kterážto metoda vrátí ukazatel do interního úložiště onoho řetězce. - Jelikož jsme
std::string
zx_n_times(2)
nikde neuložili, tak po skončení příkazu zanikne (je zavolán jeho destruktor, který uvolní interní úložiště).
Takže po skončení příkazu máme ukazatel na paměť, která již byla uvolněná... Jajks!
Co s tím?
Řešením je si navrácený std::string
uložit, aby žil po celou dobu, co jej (resp. jeho vnitřnosti) používáme. Např. takto:
// OK auto s1 = x_n_times(2); auto cs1 = s1.c_str(); auto s2 = x_n_times(3); auto cs2 = s2.c_str(); needs_c_strings(cs1, cs2);
Nyní již bude vše v pořádku.
Proč program někdy nespadne, ale vytiskne nekorektní výsledek?
Co je však zajímavější, tak někdy se lze setkat s tím, že program nespadne, valgrind
žádnou chybu nezahlásí, ale program místo 5 vytiskne 6. Huh. Jak to?
Důvodem je, že po skončení
auto cs1 = x_n_times(2).c_str();
dojde k uvolnění paměti pro dočasný std::string
, která ale hned následně může být využita pro konstrukci druhého std::string
z řádku
auto cs2 = x_n_times(3).c_str();
Oba ukazatele cs1
i cs2
tak ukazují na stejnou paměť (pro druhý řetězec), která ukazuje na řetězec o velikosti 3 znaky. Proto součet délek obou Céčkových řetězců dá 6 místo 5.
Když si zkusíte vytvořit dva řetězce, jejichž délka se velmi liší (a není tak možné znovu využít paměť z prvního řetězce), tak program již typicky spadne (či minimálně valgrind
zahlásí chybu):
auto cs1 = x_n_times(10).c_str(); auto cs2 = x_n_times(100).c_str();
To jen tak pro úplnost, kdyby na to náhodou někdo narazil a divil se, proč mu první program nepadá, ale dává nekorektní výsledek :).
(De)motivační příklad č. 2 (std::vector)
Mějme následující kód:
std::vector<int> foo() { return {1, 2, 3, 4, 5}; } int main() { for (auto i = foo().begin(), e = foo().end(); i != e; ++i) { std::cerr << *i << '\n'; } }
Pokud jej přeložíme a spustíme, tak program buď spadne, nebo vypíše nekorektní výsledek. Důvod je ten, že každý z iterátorů ukazuje do jiného vektoru. Funkce foo()
totiž vrací výsledek hodnotou, a tudíž se při každém zavolání vytvoří nový vektor.
Řešením je si výsledek funkce foo()
před iterací uložit. Lze to udělat buď takto
auto v = foo(); for (auto i = v.begin(), e = v.end(); i != e; ++i) { std::cerr << *i << '\n'; }
nebo s využitím range-based for loop takto (doporučuji):
for (auto x : foo()) { std::cerr << x << '\n'; }
Kromě toho, že je kód nyní korektní, je i čitelnější.
Zdrojové kódy
Všechny zdrojové kódy jsou k dispozici u mě na GitHubu, takže si vše můžete vyzkoušet.