2
\$\begingroup\$

I have a resource (e.g., current program configuration) that I want to handle in a single class instance, but want to hide the fact that there is, in effect, a singleton class by using a proxy class.

Reasons for not using a singleton directly include: don't want users of the class to have to change code if I move away from singleton implementation, and want to be able to have control over initialization and destruction order (which would be difficult with static classes, for example).

Additionally, this is a multithreaded application, so new instances of the Proxy class may be created at the same time (thus the mutex protecting creation of the Implementation).

The Proxy class is the proxy, and Impl class is the hidden singleton implementation. Note that I don't do anything special to Impl to make it a singleton. It is only a singleton because I track its existence in Proxy with a static member variable.

Proxy.h

#pragma once
#include <memory>
class Proxy {
 public:
   Proxy();
   void method1() //inline dispatch to hidden single class
   {
      data_->method1();
   }
 private:
   class Impl { //this class will be created once only
   public:
      void method1();
      Impl();
      ~Impl() = default;
   };
   std::shared_ptr<Impl> Make_Impl() const;
   const std::shared_ptr<Impl> data_{};
   inline static std::weak_ptr<Impl> impls_{}; //tracks whether Impl is created
};

Proxy.cpp

#include "Proxy.h"
#include <mutex>

Proxy::Proxy() : data_(Make_Impl()) {}

std::shared_ptr<Proxy::Impl> Proxy::Make_Impl() const
{ // note that impls_ may be accessed by another thread with non-const member function
  // (assignment) if it is running slightly ahead of this thread with an uninitialized impls_ so
  // need mutex before shared_ptr.lock call. if we use an atomic weak pointer (C++20) can call
  // shared_ptr.lock before mutex, check for success, and only run mutex (and try lock again) after
  // failure
   static std::mutex mtx;
   auto lock = std::lock_guard(mtx);
   std::shared_ptr<Impl> internal_data = impls_.lock(); //if implementation exists, assign to shared_ptr
   if (!internal_data) { //need to create implementation
      const auto new_impl(std::make_shared<Impl>());
      internal_data = new_impl;
      impls_ = new_impl;
   }
   return internal_data;
}

Proxy::Impl::Impl()
{
   // constructor for Impl here
}

void Proxy::Impl::method1()
{
   // the real method1 for the implementation
}

General comments welcome. Specific questions include:

  • is there a way to eliminate the pointer dereference that will occur with each call intended for the underlying implementation? (For example, When Proxy::method1 is called, that redirects to the underlying singleton class.)
  • Instead of using a static weak_ptr, is there a better way of tracking whether the Implementation exists?
  • Have others done anything like this? I'm concerned that I haven't seen this pattern (with smart pointers) documented anywhere. It makes me feel like I've gone "off the rails" with this code and have overlooked some problems with this approach.
\$\endgroup\$
1
  • \$\begingroup\$ There's no stopping some users from having to change their code in response to you changing yours. Admittedly, those are users you'll probably never keep happy. (You noticed the value was always the same so you optimized it by calling the routine once and caching it forever? Sigh.) \$\endgroup\$
    – Ed Grimm
    Feb 2, 2019 at 3:49

1 Answer 1

1
\$\begingroup\$

Without locking, this works for me:

class proxy
{
public:
    proxy()
      : m_impl(make_impl()) {}

    void method()
    {
        m_impl.method();
    }
private:
    struct Impl
    {
        Impl() {}
        ~Impl() {}
        void method() {}
    };

    Impl& m_impl;
    Impl& make_impl()
    {
        static Impl impl;
        return impl;
    }
};

When locking, you need to synchronize access to the static impl in make_impl and you need to secure the call to m_impl.method().

\$\endgroup\$
4
  • \$\begingroup\$ Nice. My only concern is destruction--my understanding is that Impl::impl inside Impl::make_impl (and thus the single instance of Impl) will be destroyed at the end of the program in an unspecified order in relation to other statics. This may cause problems with my model -- e.g., I hold open an error log file until everything else is destroyed. \$\endgroup\$
    – rsjaffe
    Feb 1, 2019 at 22:44
  • \$\begingroup\$ statics are destructed in the reverse order of their creation. So, if your logger has a dependecy on the proxy you just need to ensure that the proxy is created first. \$\endgroup\$
    – Cornholio
    Feb 1, 2019 at 22:50
  • 1
    \$\begingroup\$ Just a small thing: when locking you do NOT need a lock in make_impl() with any modern C++ compiler. \$\endgroup\$ Feb 2, 2019 at 16:01
  • \$\begingroup\$ @rsjaffe The order of destruction is very deterministic and well defined (Reverse of creation). You can use this fact to force a destruction order. See: stackoverflow.com/a/335746/14065 \$\endgroup\$ Feb 6, 2019 at 18:43

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