Better Singleton classes C++

From OPeNDAP Documentation
⧼opendap2-jumptonavigation⧽

We use lots of Singleton classes in the BES Framework. One issue with that pattern is that memory is usually not returned to the heap before the process exits, leaving tools like valgrind to report the memory as leaked. This is misleading and can be ignored, except that it's a great way to hide real leaks behind the noise in a sea of false positives. Here's a way around that using C++'s unquie_ptr.

The basic plan

I'll use a real example of this from the BES; the BES 'Keys' key-value-pair configuration database. The idea is that we store a static pointer to the single copy of the class to be built. We access that pointer whenever the class is to used with a static method. That method returns a pointer to the class if it has been allocated or allocates a pointer and constructs the object if not. The pattern builds the single instance in a way that is thread safe (using the C++ 11 concurrency features).

What goes in the header

In the header for the singleton class, include a static member that is a unique pointer to an instance of the object.

class TheBESKeys: public BESObj {

    ...

    TheBESKeys() = default;

    explicit TheBESKeys(const std::string &keys_file_name);

    static std::unique_ptr<TheBESKeys> d_instance;

public:

    /// Access to the singleton.
    static TheBESKeys *TheKeys();

    ~TheBESKeys() override = default;

    ...
}

To adapt most existing code to this pattern, move an existing static pointer so that it is a private static std::unique_ptr<class>.

What goes in the implementation

Define the singleton instance a global/file level scope:

std::unique_ptr<TheBESKeys> TheBESKeys::d_instance = nullptr;

The accessor for the static pointer to the instance is likely the only code that needs to be changed in the implementation. Note that the std::once_flag is defined as static in the code block and that if the constructor that builds the object needs arguments, the should be 'captured' by the lambda.

TheBESKeys *TheBESKeys::TheKeys()
{
    if (d_instance == nullptr) {
        static std::once_flag d_euc_init_once;
        std::call_once(d_euc_init_once, []() {
            d_instance.reset(new TheBESKeys(get_the_config_filename()));
        });
    }

    return d_instance.get();
}

Note that the constructor for the class is called inside the reset() method of the uniqe_ptr object: d_instance.reset(new TheBESKeys(get_the_config_filename())); and that that is called inside std::call_once(). The use of std::call_once() ensure that if two threads simultaneously call the accessor, only one instance will be made. In the code above, a lambda was used to pass the 'runnable' to call_once(). If the pointer to the instance (d_intance) is not null, then the accessor uses unique_ptr::get() to return the 'raw' pointer to the instance, which will greatly simplify using this pattern with our existing code.