打开导航 打开导航 Open menu
量化交易系统开发(C++)/ 期权策略与风控 / 行业研究分析
量化交易系统開發(C++)/ 期权策略与风控 / 行业研究分析
C++ trading systems / options strategy & risk controls / equity research
adrian@adrianxv.cn
文章目录

Smart Pointers in C++ Smart Pointers in C++ Smart Pointers in C++

Question 51 ~ 57 from the book Daily C++ Interview

Explain RAII

RAII means resource acquisition is initialization. Actually, the term RAII could be better framed by Scope Bound Resource Management. Ownership of a piece of resource is established during initialization, and released during destruction, which is when a scope ends or an exception is thrown. This is apparant with a handle: the handle’s ownership of a piece of resource is established during initialization, and released during destruction.

C++ uses stack unwinding to ensure that resource is released when an exception is thrown. C++ traces upward along the stack until a matching catch handler is found. Along the way, C++ invokes the destructors of local objects. That’s why by wrapping any resource with a handler and calling delete to that resource in the handler’s destructor, we ensure that even if an exception is thrown, the resource is still released.

class PointerWrapper {
public:
    PointerWrapper(resource) : resource_(resource) {}
    ~PointerWrapper() { delete resource_; }

private:
    Resource* resource_;
};

On the other hand, if a matching catch handler is never found, C++ calls std::terminate() and thus std::abort and destructors are never called. But the OS cleans up all the memory used by the process.

std::unique_ptr

A std::unique_ptr object contains a pointer and a deleter. The deleter is an empty object (because the deleter only has to perform deletion and stores no data member). Normally, the size of an empty class is 1 byte. Because it ensures that distinct objects of the same empty class can have distinct addresses (especially in a container). However, since we are not in a container, C++ optimizes the default deleter so that it takes up no space. Thus, the size of std::unique_ptr is the same as a pointer.

template<typename T>
class std::unique_ptr {
    T* p;
    std::default_delete<T> deleter;
};

template<typename T>
struct std::default_delete {
    void operator()(T* ptr) {
        delete ptr;
    }
};

You can use a custom deleter by manually constructing a std::unique_ptr object with 2 arguments. But you can’t use make_unique() if you use custom deleter.

template<typename T>
auto custom_deleter = [](T* p){
    // some other custom operations
    delete p;
};

std::unique_ptr<A, decltype<custom_deleter<A>>> p {new A{}, custom_deleter<A>};

std::unique_ptr also supports polymorphism, so you can use std::unique_ptr<Base> as the return type for a factory function, which returns unique_ptr’s to derived types depending on function arguments.

Since std::unique_ptr’s cannot be copied, you have to move it to convert it into a std::shared_ptr

std::unique_ptr<A> unique = make_unique<A>();
std::shared_ptr<A> shared = std::move(unique);

std::shared_ptr

An std::shared_ptr object is twice the size of a std::unique_ptr object, because it has to save not only a pointer to an object, but also a pointer to a dynamically allocated area to store reference counts. std::make_shared() allocates memory for both at once, whereas if you use new to create a std::shared_ptr, you’d allocate memory twice.

Like std::unique_ptr, you can use a custom deleter.

std::shared_ptr might cause cyclic dependency problems. When foo() ends, the pointers a and b are destroyed, but the 2 object still lives and hold shared_ptrs that point at each other. Now, there is memory leak, because you can’t visit and release the memory of the object with any pointer.

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> previous;
};

void foo() {
    std::shared_ptr<Node> a = make_shared();
    std::shared_ptr<Node> b = make_shared();

    a->next = b;
    b->previous = a;
}

In this case, if you’d change next and previous to std::weak_ptr, the object will just destruct when the scope ends and we are free from memory leak.

std::weak_ptr

std::weak_ptr::lock either returns a shared_ptr to the pointed object, or a nullptr if the std::shared_ptr expires.

When not to use smart pointers

Smart Pointers expresses ownership semantics. The ownership is either unique or shared. However, functions that just invoke some behavior through a pointer doesn’t care about who owns the object. Thus, using smart pointers in their parameters unnecessarily couples the API with ownership semantics. That’s not good, just use a raw pointer in the API.

In fact, using raw pointers doesn’t necessarily mean using new. And if we avoid using new, using raw pointers (mostly) won’t cause memory leak.

Should we initialize a pointer to nullptr

In most cases, it’s better to initialize a pointer to nullptr at declaration time. By doing so, we explicitly set a default value that we can test (if (p == nullptr)), and it’s much easier to debug bugs caused by dereferencing a nullptr than bugs caused by a dereferencing a pointer that points to a random address (which causes obscure bugs!).