I have been around the C++ community for a while to hear that raw pointers "are evil" and that they should be avoided as much as possible. While one of the main reasons to use smart pointers over raw pointers is to "prevent" memory leaks. So my question is: Even when using smart pointers, is it still possible to have memory leak ? If yes how will that be possible ?
Even when using smart pointers, is it still possible to have memory leak ?
Yes, if you are not careful to avoid creating a cycle in your references.
If yes how will that be possible ?
Smart pointers based on reference counting (such as shared_ptr) will delete the pointed-to-object when the reference count associated with the object drops to zero. But if you have a cycle in your references (A->B->A, or some more elaborate cycle), then the reference counts in the cycle will never drop to zero because the smart pointers are "keeping each other alive".
Here is an example of a simple program that leaks memory despite using only using shared_ptr for its pointers. Note that when you run it, the constructors print a message but the destructors never do:
#include <stdio.h>
#include <memory>
using namespace std;
class C
{
public:
C() {printf("Constructor for C: this=%p\n", this);}
~C() {printf("Destructor for C: this=%p\n", this);}
void setSharedPointer(shared_ptr<C> p) {pC = p;}
private:
shared_ptr<C> pC;
};
int main(int argc, char ** argv)
{
shared_ptr<C> pC(new C);
shared_ptr<C> pD(new C);
pC->setSharedPointer(pD);
pD->setSharedPointer(pC);
return 0;
}
In addition to having circular references, another way to leak smart pointers is by doing something rather innocent looking:
processThing(std::shared_ptr<MyThing>(new MyThing()), get_num_samples());
A person who is vaguely familiar with C++ might assume that function arguments are evaluated from left to right. This is a natural thing to think, but unfortunately it is wrong (RIP intuition and principle of least surprise). In fact, only clang
guarantees left to right function argument evaluation (AFAIK, maybe it is not a guarantee). Most other compilers evaluate from right to left (including gcc
and icc
).
But regardless of what any specific compiler does, the C++ language standard (except for C++17, see end for details) does not dictate in what order arguments are evaluated, so it's entirely possible for a compiler to evaluate function arguments in ANY order.
From cppreference:
Order of evaluation of the operands of almost all C++ operators (including the order of evaluation of function arguments in a function-call expression and the order of evaluation of the subexpressions within any expression) is unspecified. The compiler can evaluate operands in any order, and may choose another order when the same expression is evaluated again.
Therefore, it is entirely possible that the processThing
function arguments above are evaluated in the following order:
new MyThing()
get_num_samples()
std::shared_ptr<MyThing>()
This may cause a leak, because get_num_samples()
may throw an exception, and so std::shared_ptr<MyThing>()
may never be called. Emphasis on may. It is possible according to the language specification, but I actually haven't seen any compiler do this transformation (admittedly gcc/icc/clang are the only compilers I use at the time of writing). I wasn't able to force gcc or clang to do it (after about an hour of trying/researching I gave it up). Maybe a compiler expert could give us a better example (please do if you're reading this and are a compiler expert!!!).
Here's a toy example where I am forcing this order using gcc. I cheated a bit, because it turns out it's difficult to force the gcc compiler to arbitrarily reorder the argument evaluation (it still looks pretty innocent, and it does leak as confirmed by some messages to stderr):
#include <iostream>
#include <stdexcept>
#include <memory>
struct MyThing {
MyThing() { std::cerr << "CONSTRUCTOR CALLED." << std::endl; }
~MyThing() { std::cerr << "DESTRUCTOR CALLED." << std::endl; }
};
void processThing(std::shared_ptr<MyThing> thing, int num_samples) {
// Doesn't matter what happens here
}
int get_num_samples() {
throw std::runtime_error("Can't get the number of samples for some reason...and I've decided to bomb.");
return 0;
}
int main() {
try {
auto thing = new MyThing();
processThing(std::shared_ptr<MyThing>(thing), get_num_samples());
}
catch (...) {
}
}
Compiled with gcc 4.9, MacOS:
Matthews-MacBook-Pro:stackoverflow matt$ g++ --version
g++-4.9 (Homebrew GCC 4.9.4_1) 4.9.4
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Matthews-MacBook-Pro:stackoverflow matt$ g++ -std=c++14 -o test.out test.cpp
Matthews-MacBook-Pro:stackoverflow matt$ ./test.out
CONSTRUCTOR CALLED.
Matthews-MacBook-Pro:stackoverflow matt$
Note that DESTRUCTOR CALLED
is never printed to stderr.
Easiest way to fix is to call std::make_shared
instead of instantiating a std::shared_ptr
directly.
If std::make_shared
is not available, you can also fix this issue by ensuring that you have a different statement for creating a shared_ptr
, and then pass that result to a function. This works because a compiler does not have (much) latitude between different statements (as opposed to within the same statement). Here's how you'd fix the toy example above:
// ensures entire shared_ptr allocation statement is executed before get_num_samples()
auto memory_related_arg = std::shared_ptr<MyThing>(new MyThing());
processThing(memory_related_arg, get_num_samples());
P.S. This is all stolen from "Effective C++", Third Edition, by Scott Meyers. Definitely a book worth reading if you use C++ on a daily basis. C++ is hard to get right, and this book does a nice job of giving good guidelines on how to get it more right. You can still get it wrong following the guidelines dogmatically, but you'll be a better C++ dev knowing the strategies in this book.
P.S.S. C++17 fixes this problem. See here for details: What are the evaluation order guarantees introduced by C++17?
std::make_shared
, which performs the new
internally and thus makes the problem you describe impossible. –
Lawtun std::make_shared
didn't always exist back in the TR1 smart pointer days, which is where this comes really comes from. Believe it or not, there are still folks using very old compilers where the above answer is actually useful and make_shared
is not available (I was one of those people until circa 2018) –
Pandect There are functions that release the memory from the smart pointer. In this case you are asking the smart pointer to stop managing the memory. After that, it's up to you not to leak the memory
© 2022 - 2024 — McMap. All rights reserved.