I’m sure everybody hates it when some obscure bug happens which then consumes precious development time in the attempt to fix. It happens to the best of us.

To keep these at a minimum I employ some simple rules or guidelines and you should do it too if you value your time.

Prefer references instead of raw pointers

A reference can’t be null like a pointer and you don’t have to check it if it’s non-null before using it. But it can become dangling if the object that the reference is bound to dies while you still use that reference so you have to be careful about objects lifetimes.

Pro tip: you can use a const reference to extend the lifetime of a temporary:

std::string foo() {
    return "Hello";
}

int main() {
    const std::string& ref = foo();
    std::cout << ref << std::endl;
}

Prefer smart pointers instead of raw pointers

Smart pointers leverage the RAII idiom. At their core they acquire ownership over a resource on constructor and release that resource on destructor. They behave like normal pointers with the added benefits of a RAII wrapper.

std::unique_ptr for unique ownership

If you don’t need to share your resource with multiple entities then std::unique_ptr is what you’re looking for. Because this type of smart pointer allows only unique ownership you need to know that it only supports move semantics i.e. you’re only allowed to move from one unique pointer to another.

std::shared_ptr for shared ownership

Maybe you build some kind of logger and you want one instance of that logger to be used by multiple entities in your program, no problem you can use std::shared_ptr for that. Unlike std::unique_ptr we can copy this type of smart pointer; when doing that we simply create another std::shared_ptr that points to the same resource.

std::shared_ptr is a bit heavier that its brother because a) internaly it must allocate extra memory for the reference count block and b) when a copy is created/deleted that reference counter is atomically modified to reflect the number of owners a resource has.

Note: only the reference counter is atomic and can be accessed safely in a concurrent environment, but the user must make sure that the resource is thread safe:

struct resource {
    void modify_resource() {
        ++x;
    }

    int x = 0;
};

int main() {
    // This is safe:
    auto sptr0 = std::make_shared<resource>();
    auto sptr1 = sptr0;

    // Thread B
    auto thread = std::jthread([sptr1](){
        // This is safe:
        auto sptr2 = sptr1;
        // This is not safe:
        sptr2->modify_resource();
    });

    // Thread A
    sptr0->modify_resource();

}

resource::modify_resource() can run at the same time thus we have a race condition, to fix it we need to have resource use a mutex or some other kind of synchronization primitive.

In the context of shared ownership you’ll also hear about std::weak_ptr. In holds a “weak reference” aka a non owning reference and can be used to break circular references.

Create RAII wrappers for automatic resource management

We can tweak both std::unique_ptr and std::shared_ptr via their template parameters to call whatever “release” function we need, but sometimes we might not be able to do that. If we are in such a situation we can always roll our own RAII resource managers; it’s simple and effective: assume ownership of the resource in constructor, release the resource on destructor and be extra carefull with copy/move semantics because they will define the kind of ownership that wrapper will have over the resource.

For example you can think of std::jthread as a unique owner of a native thread; it doesn’t have to always be pointers.

Another advantage of this is cleaner code, you don’t have to write a bunch of duplicate code that checks if the resource must be released, you just delegate that to the wrapper’s destructor.

Use RAII lock guards to avoid deadlocks

If you’re doing a lot of multithreading programming you’ll have at some point some entity that needs to be accessed concurrently and you’ll end up using a mutex but the way you lock/unlock the mutex matters. Doing the lock/unlock manually sometimes may be necessary but in 99% of cases just use a std::lock_guard for automatic lock/unlock because you don’t want to miss on that unlock, do you?

Declare variables const by default

I really wish this was assumed automatically by the compiler. Anyway if you deal with some long piece of code like a huge function (everybody does this from time to time) it really helps with code readability because you know that something isn’t supposed to change unless explicitly stated.

Always initialize your data members

One of the worst situations is to have a piece of code that sometimes works and sometimes doesn’t. That can happen if you forget to initialize some puny variable because in C++ if you don’t initialize a variable/data member then the value for that will be whatever garbage was on the stack/heap at the moment of initialization.

This becomes extra tricky when you have to deal with raw pointers, because sometimes you may not be able to use a smart pointer. A raw pointer left uninitialized can pass the “nullptr check” which can lead to some interesting crashes when your code dereferences such a pointer.

Prefer std::string instead of C style strings

This can be a source of major security problems, it can lead to buffer overflows making it easier for attackers to use your program to gain access to a machine or remotely execute malicious code. Having to always remember that some string has the proper length, or that you have allocated enough space for the extra terminating byte \0 is harder than you think when dealing with complex programs.

Conclusion

You’ll always hear people complain how awful C++ is, that there are so many traps … the list goes on and on and some of it is actually true becase the language is very complex and the fact that the C++ comitee wants to keep things backwards compatible doesn’t help either. All languages have their pitfalls if you choose to do things non-idiomatically, heck I bet you can run into all sorts of problems in your favourite programming language if you’re not careful or if you don’t follow the idiomatic way.