2
\$\begingroup\$

I created a little singleton-like class that gets deleted when there is no reference to it anymore (so its lifetime shall be somewhat predictable). I aimed to make it thread-safe.

#include <memory>
#include <mutex>

template< typename t_type >
struct shared_instance
{
    static std::shared_ptr< t_type > get_shared_instance()
    {
        static std::weak_ptr< t_type > weak;
        static std::mutex mutex;

        auto result = weak.lock();
        if( nullptr == result )
        {
            std::unique_lock< std::mutex > lock{ mutex };

            result = weak.lock();
            if( nullptr != result )
            {
                return result;
            }

            result = std::make_shared< t_type >();
            weak = result;
        }
        return result;
    }
};

template< typename t >
struct context 
{ 
    context() 
    : instance{ shared_instance< t >::get_shared_instance() } 
    { }

private: std::shared_ptr< t > instance;
};

I would be glad if you could comment on the implementation itself and how I could tackle non-default-constructible objects (I know of perfect forwarding and variadic templates but I have not found an elegant way to evaluate them lazily).

\$\endgroup\$
  • \$\begingroup\$ Aren't singleton-like things usually a bad idea? What are you aiming for with this, I'm curious. \$\endgroup\$ – WooWapDaBug Jan 29 '18 at 12:58
  • \$\begingroup\$ yeah they are. however, i need a kind of context-object: using raii to call c-style functions like create (in ctor) and destroy (in dtor). i need to make sure that that context exists as long as i'm using some ressources in that context. the problem is in situations like context c1{}; /*some code here*/; context c2{}; c2 will tear down the context but it c1 should (braoder scope, calling the create-function mutliple times is not an error from the c-library's point of view). i need a way to make sure that behind the scenes there is only one call to destroy in the dtor of the last instance \$\endgroup\$ – cubber Jan 29 '18 at 14:16
  • \$\begingroup\$ so this is not really a classical singleton that is used to share an instance but rather to make sure that client-code can not accidentally misuse the library code and it remains exception-safe. if there are any other suggestions of how i could accomplish this, let me know, i'd be very happy. by the way: the returned shared_pointer is then hidden in the context-object which acts as kind of a contract meaning that the init/create/.. function was called and that it will be cleaned up extactly once when that context is not referenced anywhere anymore. \$\endgroup\$ – cubber Jan 29 '18 at 14:23
1
\$\begingroup\$

Non-Default Constructors:

Adding support for non-default constructible objects in a "lazy" fashion, could be done by adding a factory function of std::function<std::shared_ptr< t_type >()>.

template< typename t_type >
struct shared_instance
{
    using t_factory = std::function<std::shared_ptr< t_type >()>;

    static t_factory factory; // MUST be set before calling get_shared_instance

    static std::shared_ptr< t_type > get_shared_instance()
    {
        ...


            assert(factory); // or throw, or return empty shared_ptr

            result = factory(); // instead of make_shared

        ...
    }

};

// and define the static member...
template< typename t_type >
typename shared_instance< t_type >::t_factory shared_instance< t_type >::factory;


...

int main()
{
    context<int> c; // not ok... haven't set factory yet!

    // do this before any call to get_shared_instance
    shared_instance<int>::factory = [] () { return std::make_shared<int>(5); }; // store things in the lambda by value, not reference...

    context<int> c; // ok
}

I'm not sure this counts as elegant though, as it gives you a two-step initialization process. This could be fixed by recognising the shared_instance itself as a kind of factory, making its contents non-static, and passing it into the contexts:

template< typename t >
struct context
{
    context(instance_provider< t >& provider): // now it can't be created unless this exists...
        instance{ provider.get_shared_instance() }
    { }

private:

    std::shared_ptr< t > instance;
};

int main()
{
    auto provider = instance_provider<int>([] () { return std::make_shared<int>(5); });

    context<int> c(provider);
}

Further Considerations:

Does the C library you're using say anything about context lifetime itself? (e.g. Winsock reference counts calls to WSAStartup() and WSACleanup() internally, so as long as the calls are paired properly the C library handles cleaning up)

The main feature that this shared_instance class seems to provide is destroying The Instance promptly after the last context is destroyed. RAII itself could be achieved by just storing a unique pointer over the maximum possible lifetime of any contexts, which would be simpler. Is this prompt destruction actually important?

If not, then it's just a singleton, which we either live with, or do the work to pass a pointer or reference to the thing you need where it needs to be.

\$\endgroup\$
  • \$\begingroup\$ if i understood your second approach correctly, i could just create context<> c1{provider} and context<> c2{provider} which would both call the ctor of the context-type and the dtor of the context-type, so it might happen that objects have a context-member created from c1 while c2 already called the teardown-function, right? passing in a lambda for lazy execution is a good idea when retreiving the parameters of the lambda is more expensive than the whole concept itself, setting the required params statically before calls to get_shared_instance() is propably cheapest (and the ugiest one) :) \$\endgroup\$ – cubber Feb 8 '18 at 11:28
  • \$\begingroup\$ i'm still marking your answer as accepted because of your efforts and additional input in further considerations. after all, my question was how to decorate an anti-pattern which can not be done much nicer than you suggested i suppse, thanks again for all your effort and considerations! \$\endgroup\$ – cubber Feb 8 '18 at 11:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.