Search code examples
c++c++11c++14template-meta-programmingtype-erasure

How to implement a copy constructor for a class with type-erased data member?


What I am trying to achieve: I want a class to hold configuration for my program, something similar to boost::options, but boostless. It should be used like that:

auto port = Config.Get<int>(Options::Port);
Config.Set<Options::Port>(12345);

To hold the exact values I use type erasure pattern (is it a pattern?) in Any class (yeah, like boost::variant/any, but boostless). So my classes look like that:

#include <memory>
#include <map>
#include <mutex>

enum class Options {
  kListenPort = 0,
  kUdsPath,
  kConfigFile,
};

class AnyData;
class AnyDataBase;
class Any {
 public:
  template <typename T> Any(const T& any) : data_{any} {}
  Any(const Any& any) : data_{std::make_unique<AnyDataBase>(any.data_.get())} {}; // THIS IS WHERE I GOT DESPERATE
  ~Any(){}
  template <typename T> inline const T& As() const {
    if (typeid(T) != data_->type_info()){
      throw std::runtime_error("Type mismatch.");
    }else{
      return static_cast<std::unique_ptr<AnyData<T>>>(data_)->data_;
    }
  }

 private:
  struct AnyDataBase {
    virtual ~AnyDataBase(){}
    virtual const std::type_info& type_info() const = 0;
  };

  template <typename T> struct AnyData : public AnyDataBase {
    AnyData(const T& any_data) : data_{any_data} {}
    const inline std::type_info& type_info() const {
      return typeid(T);
    }
    T data_;
  };

  std::unique_ptr<AnyDataBase> data_;
};

class Option {
 private:
  Option(Any& value) : value_{value} {}
  Option() = delete; // we want the user to provide default value.
  ~Option(){};
  template <typename T> inline const T& Get() const {
    return value_.As<T>();
  }

 private:
  bool is_mandatory_;
  bool is_cmdline_;
  //TODO: add notifier
  Any value_;
};

using OptionsPair = std::pair<Options, std::shared_ptr<Option>>;
using OptionsData = std::map<Options, std::shared_ptr<Option>>;

class IConfig {
 public:
  virtual void Load(const std::string& filename) = 0;
};

class Config : public IConfig {
 public:
  Config(int argc, char** argv);
  ~Config() {};
  void Load(const std::string& filename);
  template <Options O> void Set(const Any& value);
  template <typename T> const T& Get(Options option);

 private:
  std::unique_ptr<OptionsData> data_;
  mutable std::mutex mutex_;
};

And when I use it like that...

template <Options O> void Config::Set(const Any& value) {
  std::lock_guard<std::mutex> lock(mutex_);

  if (data_->find(O) == data_->end()) {
    data_->insert(std::pair<Options, std::shared_ptr<Option>>(O, value));
    // TODO: i don't get in why it doesn't work this way:
    data_->insert(OptionsData {O, std::make_shared<Option>(value)});
  } else {
    data_->at(O) = std::make_shared<Option>(value);
  }
}

... I need Any class to have a copy constructor (I hope someone would point how I can avoid that).

And, as you can see from the comment on the copy constructor I don't know how to make it, since it is not templated. And I don't know how to create a new unique_ptr without knowing the type of the value, which is help in the source unique_ptr, I want to copy the value from.

The error is:

In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/memory:81:0,
                 from Config.h:5,
                 from Config.cpp:2:
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/unique_ptr.h: In instantiation of ‘typename std::_MakeUniq<_Tp>::__single_object std::make_unique(_Args&& ...) [with _Tp = Any::AnyDataBase; _Args = {Any::AnyDataBase*}; typename std::_MakeUniq<_Tp>::__single_object = std::unique_ptr<Any::AnyDataBase>]’:
Config.h:23:76:   required from here
/usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/unique_ptr.h:765:69: error: invalid new-expression of abstract class type ‘Any::AnyDataBase’
     { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
                                                                     ^
In file included from Config.cpp:2:0:
Config.h:36:10: note:   because the following virtual functions are pure within ‘Any::AnyDataBase’:
   struct AnyDataBase {
          ^
Config.h:38:35: note:   virtual const std::type_info& Any::AnyDataBase::type_info() const
     virtual const std::type_info& type_info() const = 0;

Update: Just in case anybody finds this topic interesting or useful. If i got it right one cannot simply cast a unique_ptr like that:

static_cast<std::unique_ptr<AnyData<T>>>(data_)->data_;

The most clear solution that i found so far https://stackoverflow.com/a/21174979/2598608 Resulting code looks like that:

return static_unique_ptr_cast<AnyData<T>, AnyDataBase>(std::move(data_))->data_;

And you have to make unique_ptr member mutable or remove const qualifier from the Get() method, because the static_unique_cast<>() extracts the original deleter from the source unique_ptr, therefore modifying it.


Solution

  • Essentially the Any class needs to know how to create a deep copy of the type erased AnyDataBase. It can't really to that since the AnyDataBase is abstract. It needs the help to the AnyDataBase to get that right.

    One technique is to implement a "clone" method in the AnyDataBase. There are various signatures that this function could take, but since you are already using std::unique, it may be easiest to continue with its usage as follows;

    std::unique_ptr<AnyDataBase> clone() const;
    

    A sample implementation of in the Any class;

    class Any {
     public:
      template <typename T> Any(const T& any) : data_{std::make_unique<AnyData<T>>(any)} {}
      Any(const Any& any) : data_{any.data_->clone()} {}; // use the clone
      ~Any(){}
      template <typename T> inline const T& As() const {
        if (typeid(T) != data_->type_info()){
          throw std::runtime_error("Type mismatch.");
        }else{
          return static_cast<std::unique_ptr<AnyData<T>>>(data_)->data_;
        }
      }
     private:
      struct AnyDataBase {
        virtual ~AnyDataBase(){}
        virtual std::unique_ptr<AnyDataBase> clone() const = 0; // clone already as std::unique
        virtual const std::type_info& type_info() const = 0;
      };
      template <typename T> struct AnyData : public AnyDataBase {
        AnyData(const T& any_data) : data_{any_data} {}
        std::unique_ptr<AnyDataBase> clone() const override { return std::make_unique<AnyData<T>>(data_); }
        const inline std::type_info& type_info() const override { return typeid(T); }
        T data_;
      };
      std::unique_ptr<AnyDataBase> data_;
    };
    

    When attempting to copy the Any class, it in turn calls clone() on the AnyDataBase which then (in AnyData) creates a full copy of the data_ member (of type T) and returns the required std::unique.

    Here is a sample of it.

    Note: std::unique_ptr<AnyDataBase> clone() const override { return std::make_unique<AnyData<T>>(data_); } works as intended, the constructed unique_ptr<AnyData<T>> to converted to the return type unique_ptr<AnyDataBase> due to the an available constructor allowing the implicit conversion of the unique_ptr<>::pointer types.


    This technique is also known as virtual constructors, and usually relies on covariant return types; although the covariance is not used here in the sample above. The code could easily be changed to use the covariant return over the std::unique.

    See this answer and this one for more discussions on this.