Máté Cserép
April 2021, Budapest
const char *hello1 = "Hello World";
char *hello2 = "Other Hello";
hello1[1] = 'a'; // syntax error
hello2[1] = 'a'; // could cause runtime error
char *s = const_cast<char *>(hello1); // dangerous
s[3] = 'x'; // could be runtime error
Constant data: values are known at compile-time
This kind of data stored outside of the program read-write area. Therefore a write attempt is undefined.
Automatic life: non-static objects local to a block has automatic life.
void f()
{
int i = 2; // life starts here with initialization
/* .... */
} // life finished here
Such objects are created in the stack. The stack is safe in a multithreaded environment. Objects are created when the declaration is encountered and destroyed when control leaves the declaration block.
char *p = new char[1024]; // life starts here
/* .... */
delete [] p; // life finished here
Objects with dynamic life are created in the free store. The lifetime starts with the evaluation of a new expression (not the new operator). The life ends at the delete expression.
There are two kind of allocation: one for single objects and one for arrays. The delete operation must be corresponding with the type of the new operation.
char *p1 = new char[1024];
char *p2 = new char[1024];
delete [] p1;
delete [] p1; // could be runtime error: p1 deleted twice
delete p2; // could be runtime error: p2 points to array
The free store is often referred as the heap. This is not correct: free memory (new) and heap (malloc) is not necessarily the same.
date d(2015,3,13); // life of "d", "i" starts here
static int i;
int main() { // initialization / constr call happens here
/* ... */
} // destr call happens here
Global variables, namespace variables, and static data members have static life. Their life starts at the beginning of the program, and ends at the of it. The order of creation is well-defined inside a compilation unit, but no defined between source files.
Local statics are declared inside a function as local variables, but with the static keyword. The life starts (and the initialization happens) when the declaration first time encountered and ends when the program is finishing.
int main() {
while (/* ... */)
if (/* ... */) {
static int j = 6; // initialization happens here
}
return 0;
} // destr call happens here
Array elements are created with the array itself in order of indices, and destroyed when the array itself is deleted.
Built-in arrays by the standard must have their size known by the compiler (constant expression).
Some compiler, like GNU G++ is submissive: they accept variable size arrays with warnings.
Note: in C99 variable size arrays are accepted.
Non-static data members are created when their holder object is created.
If they have constructor, then their constructor will be woven into the container object constructor.
The sub-objects will be initialized by their constructor.
Built-in types have no constructor, so they must be explicitly initialized.
struct X {
int i; // will not be initialized
};
int main() {
X x;
x.i; // undefined
}
X::X() : i(0) { } // use initialization list!
/* Union */
union A {
int i;
double d;
char c;
}; // the whole union occupies max(sizeof(int), sizeof(double), sizeof(char))
int main()
{
A a = { 43 }; // initializes the first member, a.i is now the active member
// at this point, reading from a.d or a.c is UB
std::cout << "a.i = " << a.i << std::endl;
a.c = 'a'; // a.c is now the active member
// at this point, reading from i or d is UB but most compilers define this
std::cout << "a.i = " << a.i << std::endl; // 97 most likely
return 0;
}
void f(string &s1, string &s2, string &s3)
{
const char *cs = (s1+s2).c_str();
cout << cs; // Bad!!
if (strlen(cs = (s2+s3).c_str()) < 8 && cs[0] == 'a' ) // OK
cout << cs; // Bad!!
}
Temporary objects:
void f(string &s1, string &s2, string &s3)
{
cout << s1 + s2;
string s = s2 + s3;
if (s.length() < 8 && s[0] == 'a' )
cout << s;
}
The correct way:
Another correct way (lifetime extension):
void f(string &s1, string &s2, string &s3)
{
cout << s1 + s2;
const string &s = s2 + s3;
if (s.length() < 8 && s[0] == 'a' )
cout << s; // Ok
} // s1+s2 destroys here, when the const ref goes out of scope
namespace std
{
class bad_alloc : public exception { /* ... */ };
}
void* operator new(size_t) // operator new() may throw bad_alloc
void operator delete(void *) noexcept; // operator delete() never throws
The new operator throws std::bad_alloc, when the allocation fails.
Exceptions are not always the desired solution, sometimes we want to avoid them. The nothrow version of new operation returns null pointer rather throwing exception.
// indicator for allocation that doesn't throw exceptions
struct nothrow_t {};
extern const nothrow_t nothrow;
// nothrow version
void* operator new(size_t, const nothrow_t&);
void operator delete(void*, const nothrow_t&) noexcept;
Note: the compiler needs to free the memory it allocated via operator new with a matching call to operator delete, hence the overload for delete.
int main() {
try {
while(true) {
new int[100000000ul]; // throwing overload
}
} catch (const std::bad_alloc& e) {
std::cout << e.what() << std::endl;
}
while (true) {
int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
if (p == nullptr) {
std::cout << "Allocation returned nullptr" << std::endl;
break;
}
}
}
MyClass *mc1 = new MyClass(42); // throwing overload
MyClass *mc2 = new(std::nothrow) MyClass(42); // non-throwing overload
Usage:
More complex example:
void* operator new[](size_t);
void operator delete[](void*) noexcept;
void* operator new[](size_t, const nothrow_t&);
void operator delete[](void*, const nothrow_t&) noexcept;
There are separate operators for arrays.
typedef void (*new_handler)();
new_handler set_new_handler(new_handler new_p) noexcept;
What to do, when error occurs on allocation:
The new-handler function is the function called by allocation functions whenever a memory allocation attempt fails.
Possible options:
void* operator new(size_t, void* p) { return p; }
void operator delete(void* p, void*) noexcept { }
void* operator new[](size_t, void* p) { return p; }
void operator delete[](void* p, void*) noexcept { }
Sometimes we want to create objects without allocating new memory. There is a placement version of new operator.
struct Test
{
Test(int a, int b, int c) : a(a), b(b), c(c) { }
int a, b, c;
};
int main()
{
for(int i = 0; i < 100000000; ++i)
{
Test *t = new Test(1, 2, 3);
/* ... */
delete t;
}
return 0;
}
This is a way to optimize running time, e.g.:
struct Test
{
Test(int a, int b, int c) : a(a), b(b), c(c) { }
int a, b, c;
};
int main()
{
std::aligned_storage<sizeof(Test), alignof(Test)>::type als[1];
for(int i = 0; i < 100000000; ++i)
{
Test *t = new(als) Test(1, 2, 3);
/* ... */
t->~Test();
}
return 0;
}
The optimized code using placement new:
Result for creating 100.000.000 objects:
New and delete operators could be defined by the user in two levels:
The member new and delete is responsible for allocating objects for that certain class.
The global operator however is responsible for all memory allocation.
struct Test
{
Test(int d) : data(d) { cerr << "Test::Test" << endl; }
~Test() { cerr << "Test::~Test" << endl; }
/* static */ void* operator new(size_t sz);
/* static */ void operator delete(void* p) noexcept;
int data;
};
// member new and delete
void* Test::operator new(size_t sz)
{
cerr << "Test::new" << endl;
return ::operator new(sz);
}
void Test::operator delete(void* p) noexcept
{
cerr << "Test::delete" << endl;
::operator delete(p);
}
// global new and delete
void* operator new(size_t sz)
{
cerr << "::new" << endl;
return malloc(sz);
}
void operator delete(void* p) noexcept
{
cerr << "::delete" << endl;
free(p);
}
int main()
{
Test *t = new Test(42);
/* ... */
delete t;
return 0;
}
struct Test
{
Test(int d) : data(d) { cerr << "Test::Test" << endl; }
~Test() { cerr << "Test::~Test" << endl; }
/* static */ void* operator new(size_t sz, int d);
/* static */ void operator delete(void* p) noexcept;
int data;
};
// global new and delete
void* operator new(size_t sz)
{
cerr << "::new" << endl;
return malloc(sz);
}
void operator delete(void* p) noexcept
{
cerr << "::delete" << endl;
free(p);
}
void* operator new(size_t sz, int d)
{
cerr << "::new, data:" << d << endl;
return malloc(sz);
}
void operator delete(void* p, int d) noexcept
{
cerr << "::delete, data:" << d << endl;
free(p);
}
// member new and delete
void* Test::operator new(size_t sz, int d)
{
cerr << "Test::new, data:" << d << endl;
return ::operator new(sz, d);
}
void Test::operator delete(void* p) noexcept
{
Test* t = reinterpret_cast<Test*>(p);
cerr << "Test::delete, data:" << t->data << endl;
::operator delete(p, t->data);
}
int main()
{
int x;
while (cin >> x)
{
Test *t = new(x) Test(x);
/* ... */
delete t;
}
return 0;
}
As we can overload new and delete operators at class or global level:
we can provide extra parameters, for debug or other purposes.
struct Res
{
char* cp;
Res(int n) { cp = new char[n]; }
~Res() { delete [] cp; }
char *getcp() const { return cp; }
};
void f()
{
Res res(1024);
g(res.getcp());
h(res.getcp());
}
Stroustrup: Resource Allocation Is Initialization
But be careful:
struct BadRes
{
char* cp;
BadRes(int n) { cp = new char[n]; ... init(); ... }
~BadRes() { delete [] cp; }
void init() { ... if (error) throw XXX; ... }
};
We solve these problems with smart pointers for memory resources.
The main issue with smart pointers is that who have to free the resources? Two main strategies exist for smart pointers:
auto_ptr<int> p(new int(42));
auto_ptr<int> q;
cout << "after initialization:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;
q = p;
cout << "after assigning auto pointers:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;
*q += 13; // change value of the object q owns
p = q;
cout << "after change and reassignment:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;
The auto_ptr is a ownership-based smart-pointer. Copy constructor and assignment transfers the ownership.
template<class T>
class auto_ptr
{
private:
T* ap; // refers to the actual owned object (if any)
public:
typedef T element_type;
// constructor
explicit auto_ptr (T* ptr = 0) throw() : ap(ptr) { }
// copy constructors (with implicit conversion)
// - note: nonconstant parameter
auto_ptr (auto_ptr& rhs) throw() : ap(rhs.release()) { }
template<class Y>
auto_ptr (auto_ptr<Y>& rhs) throw() : ap(rhs.release()) { }
// assignments (with implicit conversion)
// - note: nonconstant parameter
auto_ptr& operator= (auto_ptr& rhs) throw()
{
reset(rhs.release());
return *this;
}
template<class Y>
auto_ptr& operator= (auto_ptr<Y>& rhs) throw()
{
reset(rhs.release());
return *this;
}
// destructor
~auto_ptr() throw()
{
delete ap;
}
// value access
T* get() const throw()
{
return ap;
}
T& operator*() const throw()
{
return *ap;
}
T* operator->() const throw()
{
return ap;
}
// release ownership
T* release() throw()
{
T* tmp(ap);
ap = 0;
return tmp;
}
// reset value
void reset (T* ptr=0) throw()
{
if (ap != ptr)
{
delete ap;
ap = ptr;
}
}
};
auto_ptr<int> p1(new int(1)); // Ok
auto_ptr<int> p2 = new int(2); // Error, explicit constructor
auto_ptr has explicit constructor:
Ownership strategy:
auto_ptr<int> p1(new int(1));
auto_ptr<int> p2(new int(2));
p2 = p1; // delete 2, transfers ownership from p1
Copies of auto_ptr are not equivalent:
template <class T>
void print( auto_ptr<T> p)
{
if (p.get())
cout << *p;
else
cout << "null";
}
auto_ptr<int> p(new int);
*p = 4;
print(p);
*p = 5; // runtime error
const auto_ptr<int> p(new int), q;
*p = 4;
print(p); // compile-time error: cannot change ownership
*p = 5; // ok: const of ownership
q = p; // compile-time error: q const
To avoid this mistakes we may ensure keeping the ownership:
vector< auto_ptr<int> > v;
v.push_back( auto_ptr<int>( new int(1) ) );
v.push_back( auto_ptr<int>( new int(4) ) );
v.push_back( auto_ptr<int>( new int(3) ) );
v.push_back( auto_ptr<int>( new int(2) ) );
sort( v.begin(), v.end() ); // What about the pivot element, etc...
// unique(), remove(), etc...
auto_ptr is not suitable for STL containers:
There is a so-called: source-sink strategy using auto_ptr class.
Source is a function, which creates new memory resource and transfers it back to the caller. Sink is a function, which gets the ownership from its caller, and removes the memory resource.
auto_ptr<int> source(int n)
{
auto_ptr<int> p(new int(n));
// ...
return p; // transfers ownership to the caller
}
void sink( auto_ptr<int>) { }; // removes memory resource
auto_ptr<int> p1 = source(1); // create, get ownership
// ...
sink(p1); // gets ownership and delete p1 on return
class B;
class A
{
private:
const auto_ptr<B> p1;
const auto_ptr<B> p2;
public:
A( B b1, B b2) : p1(new B(b1)), p2(new B(b2)) { }
A( const A& rhs) : p1( new B(*rhs.p1)), p2( new B(*rhs.p2)) { }
A& operator=(const A& rhs)
{
*p1 = *rhs.p1;
*p2 = *rhs.p2;
return *this;
}
};
We do not need a destructor anymore.
void f(int n)
{
auto_ptr<int> p1(new int);
auto_ptr<int> p2(new int[n]);
//...
} // delete and not delete[] for p2
The auto_ptr is not able to manage arrays, because the destructor wired-in to call non-array delete.
An array delete adapter is a solution to manage arrays by auto_ptr:
template <typename T>
class ArrDelAdapter
{
public:
ArrDelAdapter(T *p) : p_(p) { }
~ArrDelAdapter() { delete [] p_; }
// implement operators like ->, *, etc...
private:
T* p_;
};
struct X
{
X() { std::cout << "X()" << std::endl; }
~X() { std::cout << "~X()" << std::endl; }
};
int main()
{
std::auto_ptr< ArrDelAdapter<X> > pp(
new ArrDelAdapter<X>(new X[10]));
return 0;
}
Summary:
In order to fix the caveats of auto_ptr, C++11 introduced new smart pointers types:
At the same time auto_ptr became deprecated in C++11 and removed in C++17.
struct MyClass
{
MyClass() { cout << "MyClass ctor runned." << endl; }
~MyClass() { cout << "MyClass dtor runned." << endl; }
};
int main()
{
unique_ptr<MyClass> up1(new MyClass);
up1 = make_unique<MyClass>(); // since C++14
{
cout << "Entered inner block." << endl;
unique_ptr<MyClass> up2(new MyClass);
//up1 = up2; // Syntax error
up1 = move(up2); // Ownership moved from up2 to up1
up2.reset(); // No memory deallocation
cout << "Leaving inner block." << endl;
}
cout << "Left inner block." << endl;
up1.reset(); // Memory deallocation
cout << "Leaving main() function." << endl;
return 0;
}
struct MyClass
{
MyClass() { cout << "MyClass ctor runned." << endl; }
~MyClass() { cout << "MyClass dtor runned." << endl; }
};
int main()
{
shared_ptr<MyClass> sp1(new MyClass);
cout << "Use count: " << sp1.use_count() << endl;
shared_ptr<MyClass> sp2 = sp1;
cout << "Use count: " << sp1.use_count() << endl;
sp1.reset();
cout << "Use count: " << sp2.use_count() << endl;
{
cout << "Entered inner block." << endl;
shared_ptr<MyClass> sp3 = sp2;
cout << "Use count: " << sp2.use_count() << endl;
cout << "Leaving inner block." << endl;
}
cout << "Left inner block." << endl;
cout << "Use count: " << sp2.use_count() << endl;
cout << "About to delete the last shared pointer." << endl;
sp2.reset();
cout << "Deleted the last shared pointer." << endl;
return 0;
}
struct MyClass
{
MyClass() { cout << "MyClass ctor runned." << endl; }
~MyClass() { cout << "MyClass dtor runned." << endl; }
};
int main()
{
shared_ptr<MyClass> sp1 = make_shared<MyClass>(); // since C++11
cout << "Use count: " << sp1.use_count() << endl;
weak_ptr<MyClass> wp = sp1;
cout << "Use count: " << sp1.use_count() << endl; // Only shared_ptr counts,
// weak_ptr does not
cout << "Use count: " << wp.use_count() << endl;
{
cout << "Entered inner block." << endl;
shared_ptr<MyClass> sp2 = wp.lock();
cout << "Use count: " << sp1.use_count() << endl;
if(sp2)
cout << "Lock acquired." << endl;
cout << "Leaving inner block." << endl;
}
cout << "Left inner block." << endl;
cout << "Use count: " << sp1.use_count() << endl;
cout << "About to delete the last shared pointer." << endl;
sp1.reset();
cout << "Deleted the last shared pointer." << endl;
shared_ptr<MyClass> sp3 = wp.lock();
if(!sp3)
cout << "Shared object does not exists anymore." << endl;
if(wp.expired()) // Not thread safe solution.
cout << "Shared object does not exists anymore." << endl;
return 0;
}
struct MyClass
{
shared_ptr<MyClass> next;
MyClass(shared_ptr<MyClass> next)
{
this->next = next;
cout << "Constructor completed for a MyClass object." << endl;
}
virtual ~MyClass()
{
cout << "Destructor completed for a MyClass object." << endl;
}
};
int main()
{
shared_ptr<MyClass> nullp;
shared_ptr<MyClass> sp1(new MyClass(nullp));
shared_ptr<MyClass> sp2(new MyClass(sp1));
{
cout << "Entered inner block." << endl;
shared_ptr<MyClass> sp3(new MyClass(sp2));
sp1->next = sp3; // Create cyclic referencing
cout << "sp3 use count: " << sp3.use_count() << endl;
cout << "Leaving inner block." << endl;
}
cout << "Left inner block." << endl;
cout << "sp1 use count: " << sp1.use_count() << endl;
cout << "sp2 use count: " << sp2.use_count() << endl;
cout << "sp3 use count: " << sp1->next.use_count() << endl;
sp1.reset();
sp2.reset();
cout << "Released sp1 and sp2, dtors still not executed." << endl;
return 0;
}
shared_ptr<MyClass> sp(new MyClass, [](auto p) {
std::cout << "Freeing resource..." << std::endl;
delete p;
});
struct MyClassDeleter {
void operator()(MyClass* p) const {
std::cout << "Freeing resource..." << std::endl;
delete p;
}
};
shared_ptr<MyClass> sp(new MyClass, Deleter);
An optional deleter expression can be defined to implement a custom method to free the allocated resource.
Using a functor:
Using a lambda expression:
The default deleter simply frees the pointer, properly handling arrays.
MyClass* p = new MyClass;
shared_ptr<MyClass> sp1(p);
shared_ptr<MyClass> sp2(p); // bad: sp2 != sp1
shared_ptr<MyClass> sp3 = make_shared<MyClass>(); // bad: sp3 != sp1
shared_ptr<MyClass> sp4 = sp1; // good: sp4 == sp1
Creating multiple distinct shared_ptr objects with separate reference counts is a trap, which shall be avoided.
class MyClass
{
public:
std::shared_ptr<MyClass> getPtr() {
return shared_ptr<MyClass>(this);
}
};
shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->getPtr(); // bad: sp2 != sp1
shared_ptr<MyClass> sp3 = sp1->getPtr(); // bad: sp3 != sp1 && sp3 != sp2
The naive technique of just returning a shared_ptr to this leads to separate reference counts:
Instead use the enable_shared_from_this base class:
class MyClass : public std::enable_shared_from_this<MyClass>
{
public:
std::shared_ptr<MyClass> getPtr() {
return shared_from_this();
}
};
shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->getPtr(); // good: sp2 == sp1
shared_ptr<MyClass> sp3 = sp1->getPtr(); // good: sp3 == sp2 == sp1
How does it work:
Sometimes restriction of object storage can be useful:
class X {
public:
X() {}
void destroy() const { delete this; }
protected:
~X() {}
};
class Y : public X { }; // inheritance is ok
class Z { X x; }; // syntax error, use pointer
X x1; // syntax error
int main() {
X x2; // syntax error
Y y1; // ok, would be syntax error if ~X() was private
X* xp = new X; // ok
Y* yp = new Y; // ok
delete xp; // syntax error
xp->destroy(); // ok
delete yp; // ok, would be syntax error if ~X() was private
};
The following class X could be created only by new operator in the HEAP. This is important when new takes extra parameters, or defines special semantics: like persistence or garbage collection.
class X
{
protected:
static void* operator new(size_t sz);
static void operator delete(void* p) noexcept;
// static void operator delete(void *) noexcept = delete; in C++11
};
class Y : public X { }; // inheritance is ok
class Z { X x; }; // ok
X x1; // ok
int main()
{
X x2; // ok
Y y1; // ok
X* xp = new X; // syntax error
Y* yp = new Y; // syntax error
delete xp; // syntax error
delete yp; // syntax error
};