Search code examples
c++c++11templatesabstract-classcopy-constructor

copy construct a vector of objects of an abstract class that is implemented by a template class


I have an abstract class say A which is implemented by a template class say B, B is specialized for a vector type that implements a copy constructor and a copy assignment operator, but when I compile i get the error: static assertion failed: result type must be constructible from value type of input range. Declaring a virtual copy assignment in the abstract class does not help, as the signature there is: A & operator=(const A &); but in B it is implemented for type B so the signatures do not match.

Why I have this weird hierarchy? Because I am trying to implement a json parsing library, I need to have a container that can store string, number, bigint, boolean and array types, so I implemented the hierarchy to achieve type erasure, the template type is for these 5 types and need to specialize only for string and vector types.

Actual hierarchy (minimal reproducible example):

 #include <iostream>
 #include <string>
 #include <vector>
 #include <memory>
 #include <iostream>

 class BasicJsonType {
 public:
     virtual std::string toString() const = 0;
     virtual void setNull() = 0;
     // copy assignment method
     // virtual BasicJsonType& operator= (const BasicJsonType &value) = 0;
     ~BasicJsonType() = default;
 };

 template<typename T>
 class BasicJsonTypeInterface: public BasicJsonType {
 protected:
     bool empty = true;

 public:
     virtual const T& get() = 0;
     virtual void set(const T&) = 0;
 };

namespace json {
// json::array is defined as
using array = std::vector<BasicJsonType>;
}

template <typename T>
class JsonValue {
    T x;
public:
    virtual std::string toString() {
        return "";
    }

    virtual const T & get() {
        return this->x;
    }

    virtual void set(const T &value) {
        this->x = value;
    }
};

template<>
class JsonValue<json::array>: public BasicJsonTypeInterface<json::array> {
    std::shared_ptr<json::array> array;

public:
    JsonValue() = delete;

    JsonValue(const JsonValue<json::array> &value): JsonValue(*(value.array)) {
        std::cout << "const JsonValue<json::array> &";
    }

    JsonValue(const json::array &array) {
        std::cout << "const json::array &";
        // error
        this->array.reset(new json::array(array));
    }

    JsonValue(JsonValue<json::array> &&value): JsonValue(static_cast<json::array &&> (*(value.array)))
    { std::cout << "const JsonValue<json::array> &"; }

    JsonValue(json::array &&array) {
        this->array.reset(new json::array(std::move(array)));
        this->empty = false;
    }

    virtual void setNull() { }

    virtual const json::array &get() {
        return *(this->array);
    }

    virtual void set(const json::array &value) {
        this->array.reset(new json::array(value));
     }
};

int main() {}

I created the interface type since I wanted to implement the get, set methods for all the types and irrespective of the type.

I searched for the error, and what I found is I am missing a copy function for the BasicJsonType, like what it suggests here.

There maybe some design flaws in this, since it is my first try with anything of practical use with c++, I am targeting for c++11.


Solution

  • std::vector<BasicJsonType>;
    

    This is a useless type.

    BasicJsonType is an abstract class. Abstract classes are not value types. std::vector stores value types.

    std::vector expects regular types (or semiregular if you don't need copy). Abstract types are NOT regular types.

    There are a number of ways to have polymorphic regular types, but they all take work or 3rd party libraries.


    A minor issue:

     ~BasicJsonType() = default;
    

    this should be virtual.

    ...

    There are a number of ways to approach your problem of getting a regular type to store in std::vector.

    1. Store unique_ptr<BasicJsonType> in your vector. This permits moving but not assignment*.

    2. Implement a value_ptr (based off unique ptr) that understands how to (virtually) clone its contents when copied.

    3. Implement a cow_ptr that uses a shared ptr under the hood for immutable data, and does a copy-on-write.

    4. Create an any_with_interface based off std::any that guarantees the stored value matches an interface, and provides operator-> and * that returns that interface.

    5. Store a std::variant of the various kinds of json concrete types. Write a helper function to get the abstract interface (if you need it).


    As your set of supported types is closed (there are only so many json types), #5 is probably easiest.

    class BasicJsonType {
    public:
       virtual std::string toString() const = 0;
       virtual void setNull() = 0;
       virtual bool isNull() const = 0;
    protected: // no deleting through this interface
       ~BasicJsonType() = default;
    };
    
    // if we find this overload, remember to implement
    // your own to_json_string for the type in question
    template<class T>
    std::string to_json_string( T const& ) = delete;
    std::string to_json_string( std::string const& s ) { return s; }
    std::string to_json_string( double const& d )
    {
        std::stringstream ss;
        ss << d;
        return ss.str();
    }
    
    template <typename T>
    class JsonValue:public BasicJsonType {
    public:
      JsonValue() = default;
      JsonValue(JsonValue const&) = default;
      JsonValue(JsonValue &&) = default;
      JsonValue& operator=(JsonValue const&) = default;
      JsonValue& operator=(JsonValue &&) = default;
    
      JsonValue( T t ):value(std::move(t)) {}
    
      std::optional<T> value;
      std::string toString() const final {
        if (value)
          return to_json_string(*value);
        else
          return "(null)";
      }
      bool isNull() const final {
        return !static_cast<bool>(value);
      }
      void setNull() final {
        value = std::nullopt;
      }
    };
    template<class T>
    JsonValue(T)->JsonValue<T>;
    

    you create a free function to_json_string for each T you pass to JsonValue; if you don't, you get a compile-time error.

    The remaining tricky part is making a variant containing a vector of a type depending on the same variant.

    struct json_variant;
    using json_array = std::vector<json_variant>;
    struct json_variant :
        std::variant< JsonValue<double>, JsonValue<std::string>, JsonValue<json_array> >
    {
        using std::variant< JsonValue<double>, JsonValue<std::string>, JsonValue<json_array> >::variant;
        std::variant< JsonValue<double>, JsonValue<std::string>, JsonValue<json_array> > const& base() const { return *this; }
        std::variant< JsonValue<double>, JsonValue<std::string>, JsonValue<json_array> >& base() { return *this; }
    };
    
    BasicJsonType const& getInterface( json_variant const& var )
    {
        return std::visit( [](auto& elem)->BasicJsonType const& { return elem; }, var.base());
    }
    BasicJsonType& getInterface( json_variant& var )
    {
        return std::visit( [](auto& elem)->BasicJsonType& { return elem; }, var.base());
    }
    
    std::string to_json_string( json_array const& arr )
    {
        std::stringstream ss;
        ss << "{";
        for (auto&& elem:arr)
        {
            ss << getInterface(elem).toString();
            ss << ",";
        }
        ss << "}";
        return ss.str();
    }
    

    and test code:

    JsonValue<json_array> bob;
    bob.value.emplace();
    bob.value->push_back( JsonValue(3.14) );
    bob.value->push_back( JsonValue(std::string("Hello world!")) );
    std::cout << bob.toString();
    

    there we go, a value-semantics Json data type in C++.

    Live example.

    In , you can use boost::any or boost::variant. Everything I did above works with them, except the deduction guide (which is just syntactic sugar).

    All of the alternative plans also work; a value pointer, surrendering copy and using unique ptr, a cow pointer, etc.

    You can also roll your own any or variant, or find a stand-alone one, if you dislike boost.


    template<class T, class=void>
    struct has_clone_method:std::false_type{};
    template<class T>
    struct has_clone_method<T,
      decltype( void(&T::clone) )
    >:std::true_type{};
    
    template<class T,
      typename std::enable_if<!has_clone_method<T>{}, bool>::type = true
    >
    std::unique_ptr<T> do_clone( T const& t ) {
      return std::make_unique<T>(t);
    }
    template<class T,
      typename std::enable_if<has_clone_method<T>{}, bool>::type = true
    >
    std::unique_ptr<T> do_clone( T const& t ) {
      return t.clone();
    }
    
    template<class T>
    struct value_ptr:std::unique_ptr<T>
    {
      using base = std::unique_ptr<T>;
      using base::base;
      using base::operator=;
    
      value_ptr()=default;
      value_ptr(value_ptr&&)=default;
      value_ptr& operator=(value_ptr&&)=default;
    
      template<class D,
        typename std::enable_if<std::is_base_of<T, D>::value, bool> = true
      >
      value_ptr( value_ptr<D> const& o ):
        base( o?do_clone(*o):nullptr)
      {}
      template<class D,
        typename std::enable_if<std::is_base_of<T, D>::value, bool> = true
      >
      value_ptr( value_ptr<D>&& o ):
        base( std::move(o) )
      {}
    
      value_ptr( base b ):base(std::move(b)) {}
    
      value_ptr(value_ptr const& o):
        base( o?do_clone(*o):nullptr )
      {}
    
      value_ptr& operator=(value_ptr const& o) {
        if (!o)
        {
          this->reset();
        }
        else if (this != &o) // test only needed for optimization
        {
          auto tmp = do_clone(*o);
          swap( (base&)*this, tmp );
        }
        return *this;
      }
    };
    template<class T, class...Args>
    value_ptr<T> make_value_ptr( Args&&...args ) {
      std::unique_ptr<T> retval( new T(std::forward<Args>(args)...) );
      return std::move(retval);
    }
    
    
    class BasicJsonType {
    public:
       virtual std::string toString() const = 0;
       virtual void setNull() = 0;
       virtual bool isNull() const = 0;
       virtual std::unique_ptr<BasicJsonType> clone() const = 0;
       virtual ~BasicJsonType() = default;
    };
    
    using Json = value_ptr<BasicJsonType>;
    
    using JsonVector = std::vector<Json>;
    
    // your own to_json_string for the type in question
    template<class T>
    std::string to_json_string( T const& ) = delete;
    std::string to_json_string( std::string const& s ) { return s; }
    std::string to_json_string( double const& d )
    {
        std::stringstream ss;
        ss << d;
        return ss.str();
    }
    
    template <typename T>
    class JsonValue:public BasicJsonType {
    public:
      JsonValue() = default;
      JsonValue(JsonValue const&) = default;
      JsonValue(JsonValue &&) = default;
      JsonValue& operator=(JsonValue const&) = default;
      JsonValue& operator=(JsonValue &&) = default;
    
      JsonValue( T t ):value(make_value_ptr<T>(std::move(t))) {}
    
      value_ptr<T> value;
      std::string toString() const final {
        if (value)
          return to_json_string(*value);
        else
          return "(null)";
      }
      bool isNull() const final {
        return !static_cast<bool>(value);
      }
      void setNull() final {
        value = nullptr;
      }
    
      std::unique_ptr<BasicJsonType> clone() const final {
        return std::unique_ptr<JsonValue>(new JsonValue(*this));
      }
    };
    
    using JsonNumber = JsonValue<double>;
    using JsonString = JsonValue<std::string>;
    using JsonArray = JsonValue<JsonVector>;
    
    std::string to_json_string( JsonVector const& arr )
    {
        std::stringstream ss;
        ss << "{";
        for (auto&& elem:arr)
        {
            if (elem)
            {
                ss << elem->toString();
            }
            ss << ",";
        }
        ss << "}";
        return ss.str();
    }
    
    int main() {
        JsonArray arr;
        arr.value = make_value_ptr<JsonVector>();
        arr.value->push_back( make_value_ptr<JsonNumber>( 3.14 ));
        arr.value->push_back( make_value_ptr<JsonString>( "Hello World" ));
        std::cout << arr.toString() << "\n";
    }
    

    here we make value_ptr, a smart pointer that supports copy.

    It uses do_clone, which calls .clone() if it exists, and if it does not invokes their copy constructor. This permits you to make a value_ptr<T> where T is a value type, or a value_ptr<T> where T is an abstract type with a .clone() method.

    I use it for a low-quality "optional" within JsonValue itself (a nullable type).

    A JsonVector is then a vector of value_ptr<BasicJsonType>.

    A BasicJsonType is implemented in JsonValue, where it stores it data in turn in a value_ptr<T>.


    An iterative improvement would be to move the polymorphism to an internal detail.

    Have a JsonValue that stores a value_ptr to a JsonBase. The JsonStorage<T> class implements JsonBase, and is not itself nullable.

    JsonValue knows all 4 types that it can be. It provides interfaces that try-to-get the value of a specific type, and fail if you ask for the wrong type.

    This reduces indirection, and gives the result that there isn't a NULL of type double, string, array that is distinct.

    class JsonData {
    public:
       virtual std::string toString() const = 0;
       virtual std::unique_ptr<JsonData> clone() const = 0;
       virtual ~JsonData() = default;
    };
    
    using JsonPoly = value_ptr<JsonData>;
    
    template<class T>
    class JsonStorage:public JsonData {
    public:
      T value;
      std::string toString() const final {
        return to_json_string(value);
      }
      JsonStorage( T t ):value(std::move(t)) {}
    
      JsonStorage() = default;
      JsonStorage( JsonStorage const& )=default;
      JsonStorage( JsonStorage && )=default;
      JsonStorage& operator=( JsonStorage const& )=default;
      JsonStorage& operator=( JsonStorage && )=default;
    
      std::unique_ptr<JsonData> clone() const final {
        return std::unique_ptr<JsonStorage>(new JsonStorage(*this));
      }
    };
    
    struct JsonValue {
        JsonValue() = default;
        JsonValue( JsonValue const& ) = default;
        JsonValue( JsonValue && ) = default;
        JsonValue& operator=( JsonValue const& ) = default;
        JsonValue& operator=( JsonValue && ) = default;
    
        explicit operator bool() { return static_cast<bool>(data); }
    
        std::string toString() const {
            if (!data)
                return "(null)";
            else
                return data->toString();
        }
        template<class T>
        T* get() {
          if (!data) return nullptr;
          JsonStorage<T>* pValue = dynamic_cast<JsonStorage<T>*>(data.get());
          if (!pValue) return nullptr;
          return &(pValue->value);
        }
        template<class T>
        T const* get() const {
          if (!data) return nullptr;
          JsonStorage<T> const* pValue = dynamic_cast<JsonStorage<T>*>(data.get());
          if (!pValue) return nullptr;
          return &(pValue->value);
        }
    
        JsonValue( double d ):
            data( make_value_ptr<JsonStorage<double>>(d))
        {}
        JsonValue( std::string s ):
            data( make_value_ptr<JsonStorage<std::string>>(s))
        {}
        JsonValue( char const* str ):
            data( make_value_ptr<JsonStorage<std::string>>(str))
        {}
        JsonValue( std::initializer_list<JsonValue> );
    private:
      value_ptr<JsonData> data;
    };
    
    using JsonVector = std::vector<JsonValue>;
    std::string to_json_string( JsonVector const& arr )
    {
        std::stringstream ss;
        ss << "{";
        for (auto&& elem:arr)
        {
            ss << elem.toString();
            ss << ",";
        }
        ss << "}";
        return ss.str();
    }
    
    JsonValue::JsonValue( std::initializer_list<JsonValue> il ):
        data( make_value_ptr<JsonStorage<JsonVector>>( il ))
    {}
    
    
    int main() {
        JsonValue arr = {JsonValue{3.14}, JsonValue{"Hello World"}};
        std::cout << arr.toString() << "\n";
    }
    

    Live example.

    Here, given a JsonValue v (not a template), you can ask v.get<double>() which returns a pointer-to-double if and only if the value contains a double.

    JsonValue v = 3.14 works, as does JsonValue str = "hello".

    Adding new types requires a to_json_string overload, and that the type supported be regular.

    JsonValue is a polymorphic value type. The virtual stuff is all implementation details, not exposed to the end user. We do type erasure internally. It is basically a std::any, with an extra toString method.