RAII,smart pointers,copy and swap

本文最后更新于:July 31, 2020 pm

RAII,smart pointers,copy and swap

Recently,when i realize the template class of some ADTs(such as list,vectors,etc),i found that there is some idioms or routines that i have no idea about,so i read others’ blog and C++ primer.After reading,i realize the essential idea of memory management in cpp,so i write this blog to record my gain.

RAII

Definition

the follow content are copied from https://en.cppreference.com/w/cpp/language/raii

Resource Acquisition Is Initialization or RAII, is a C++ programming technique which binds the life cycle of a resource that must be acquired before use (allocated heap memory, thread of execution, open socket, open file, locked mutex, disk space, database connection—anything that exists in limited supply) to the lifetime of an object.

RAII guarantees that the resource is available to any function that may access the object (resource availability is a class invariant, eliminating redundant runtime tests). It also guarantees that all resources are released when the lifetime of their controlling object ends, in reverse order of acquisition. Likewise, if resource acquisition fails (the constructor exits with an exception), all resources acquired by every fully-constructed member and base sub object are released in reverse order of initialization. This leverages the core language features (object lifetime, scope exit, order of initialization and stack unwinding) to eliminate resource leaks and guarantee exception safety. Another name for this technique is Scope-Bound Resource Management (SBRM), after the basic use case where the lifetime of an RAII object ends due to scope exit.

RAII can be summarized as follows:

  • encapsulate each resource into a class, where
    • the constructor acquires the resource and establishes all class invariants or throws an exception if that cannot be done,
    • the destructor releases the resource and never throws exceptions;
  • always use the resource via an instance of a RAII-class that either
    • has automatic storage duration or temporary lifetime itself, or
    • has lifetime that is bounded by the lifetime of an automatic or temporary object

Move semantics make it possible to safely transfer resource ownership between objects, across scopes, and in and out of threads, while maintaining resource safety.

Classes with open()/close(), lock()/unlock(), or init()/copyFrom()/destroy() member functions are typical examples of non-RAII classes:

My own view

Simply put,the idea of RAII is because cpp will automatically call the destructor after the object are out of the scope,and the raw pointer will have many problems such as dangling references,forgetting to delete,deleting twice,memory leak,not knowing the type of the object(array or just an element),etc.A better way to resolve the above-mentioned problem is to wrap the procedure of allocating memory and freeing memory so that you won’t forget it and it is safer.The container in STL is RAII and the smart pointer is RAII too.Since the move + unique pointer is of critical importance in memory management,i want to write a blog to record some common idiom in memory management.

usage of unique pointer

  • std::unique_ptr embodies exclusive ownership semantics.
  • std::unique_ptr is thus a move-only type.
  • Only non-const unique_ptr can transfer the ownership of the managed object to another unique_ptr.
  • you can specify the deleter by adding the second parameter
  • for the shared one,you don’t need to specify the type
  • for the unique one you need to specify the type
  • it is vaild to return a unique pointer,because the unique pointer in the call stack is ready to be destroyed
auto unique_deleter = [](int* p) {
    std::cout << "unique_ptr delete:" << *p << "\n";
    delete p;
};

auto shared_deleter = [](int* p) {
    std::cout << "shared_ptr delete:" << *p << "\n";
    delete p;
};

auto shared_deleter_check_nullptr = [](int* p) {
    if (p != nullptr)
    {
        std::cout << "shared_ptr2 delete:" << *p << "\n";
        delete p;
    }
    else
    {
        std::cout << "shared_ptr2 is null\n";
    }
};
int main()
{
    {
        std::unique_ptr<int, decltype(unique_deleter)> pUniquePtr(nullptr, unique_deleter);
        std::unique_ptr<int, decltype(unique_deleter)> pUniquePtr2(new int(5), unique_deleter);
        std::shared_ptr<int> pSharedPtr(nullptr, shared_deleter_check_nullptr);
        std::shared_ptr<int> pSharedPtr2(new int(10), shared_deleter);
        std::shared_ptr<int> pSharedPtr3(nullptr, shared_deleter);
    }
}

explanation:(the sequence corresponds to the code)

  1. don’t call the unique_deleter,because the compiler will first identify whether it is nullptr

  2. call the unique_deleter,and the type of the deleter is determined at the compile time.

  3. ,4,5 no matter whether it is nullptr or not,always call the deleter,because the compiler use the reference count to determine whether to call the deleter.5 has a run time error because don’t check whether it is nullptr

output

shared_ptr delete:10
shared_ptr2 is null
unique_ptr delete:5

#include <iostream>
#include <memory>

struct Foo {
    Foo(int _val) : val(_val) { std::cout << "Foo...\n"; }
    ~Foo() { std::cout << "~Foo...\n"; }
    int val;
};

int main()
{
    std::unique_ptr<Foo> up1(new Foo(1));
    std::unique_ptr<Foo> up2(new Foo(2));

    up1.swap(up2);

    std::cout << "up1->val:" << up1->val << std::endl;
    std::cout << "up2->val:" << up2->val << std::endl;
}

output:

Foo…
Foo…
up1->val:2
up2->val:1
~Foo…
~Foo…

copy and swap

we use copy and swap for three things

  • code duplication
  • providing a strong exception guarantee.
  • avoid self copy
  • faster speed

pass by value

because we can utilize compiler to do the value copy for you.You just need to implement the move and copy constructor

i learned from stackoverflow and rewrite it

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
        mArray(mSize ? new int[mSize]() : nullptr)
    {
    }
    dumb_array(dumb_array&& other) :dumb_array() {
        swap(*this, other);
    }
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
        mArray(mSize ? new int[mSize] : nullptr)
    {
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
        ~dumb_array()
    {
        delete[] mArray;
    }
        friend void swap(dumb_array& fiest, dumb_array& second);
        dumb_array& operator =(dumb_array a) {
            swap(*this, a);
            return *this;
    }
        dumb_array& operator =(dumb_array &&a){
            swap(*this, a);
            return *this;
        }
private:
    std::size_t mSize;
    int* mArray;
};
void swap(dumb_array& first, dumb_array& second) {
    using std::swap;
    swap(first.mArray, second.mArray);
    swap(first.mSize, second.mSize);
}

you may wonder why the compiler will choose the swap implemented by ourself,because the rules of template function,our swap are more specific.

conclusion

  • use unique_ptr rather than raw pointer
  • use copy and swap to instead move constructor and copy operator
  • wrap the procedure of allocate resources by a class or a pointer.