简体   繁体   中英

C++ avoid call to abstract base class's constructor

Currently, I am building a library in C++ (using C++11 standards), and I am stuck on trying to figure out how to make my design more practical. I have the following abstract class E

template<typename K>
class E 
{
 public:
 virtual ~E() {};
 virtual void init() = 0;
 virtual void insert(const K& k) = 0;
 virtual size_t count() const = 0;
 virtual void copy(const E<Key>& x) = 0;
};

which I want to restrict users from instantiating it (ie, be an interface). E has two sub-classes that implement the corresponding methods:

template<typename K>
class EOne : public E<K>
{
 public:
 EOne() {}
 EOne(const EOne& x) {...}
 void init() override
 {
   ...
 }
 void insert(const K& v) override
 {
   ...
 }
 size_t count() const override
 {
   ...
 }
 void copy(const E<K>& o) override
 {
   ...
 }
 private: 
     // some private fields
};

and ETwo : public E<K> , which is similar to EOne . Also, there is a different class J , which has a member std::vector<E<K>> that needs to be instantiated during construction:

template<typename K>
class J
{
 public:
     J(size_t n, const E<K>& e) : v(n, e)
     {}
 private:
     std::vector<E<K>> v;
}

In essence, by getting a constant reference for a E<K> object, I want J 's constructor to use the reference to instantiate all n objects of v using e as a template (ie, call the copy constructor). As you can imagine, my goal is to have e be an object of either EOne<K> or ETwo<K> . For instance, I would make a call to J<K>::J(size_t n, const E<K>& e) the following way:

int main(int argc, char** argv)
{
    EOne<std::string> e;
    J<std::string> j(10, e); // populate v with 10 copies of e
    ...
}

However, the above does not compile and the compiler complains that I cannot instantiate abstract class (I am using vc++ but I believe I will get the same error on other compilers as well). Therefore, my question has to do on how I can overcome this problem? Do you have any suggestions on how I can make my design more practical.

Thank you

There is more than one approach to this. What follows is the most complex reasonable one. It requires lots of work in the type definitions, but leads to the cleanest "client" code that uses these types.


It is time to learn how to make a type regular.

An instance of a regular type behaves like a value. C++ algorithms and container work far better with regular types than it does with abstract types.

template<class K>
class E_interface {
public:
  virtual ~E_interface() {};
  virtual void init() = 0;
  virtual void insert(const K& k) = 0;
  virtual size_t count() const = 0;
  virtual void copy_from(const E_interface& x) = 0;
  std::unique_ptr<E_interface> clone() const = 0;
};

this is basically your E , except I added clone() .

template<class T, class D=std::default_delete<D>, class Base=std::unique_ptr<T>>
struct clone_ptr:Base {
  using Base::Base;
  clone_ptr(Base&& b):Base(std::move(b)) {}
  clone_ptr()=default;
  clone_ptr(clone_ptr&&o)=default;
  clone_ptr(clone_ptr const& o):
    clone_ptr(
      o?clone_ptr(o->clone()):clone_ptr()
    )
  {}
  clone_ptr& operator=(clone_ptr&&o)=default;
  clone_ptr& operator=(clone_ptr const&o) {
    if (*this && o) {
      get()->copy_from(*o.get());
    } else {
      clone_ptr tmp(o);
      *this = std::move(tmp);
    }
    return *this;
  }
};

The clone_ptr is a smart pointer that is a unique_ptr that knows how to copy itself by calling clone() and copy_from on the stored object. It may have some typos.

Now we write our E:

template<class K>
class E {
  clone_ptr<E_interface<K>> pImpl;
public:
  E() = default;
  E( std::unique_ptr<E_interface<K>> in ):pImpl(std::move(in)) {}
  E( E const& )=default;
  E( E && )=default;
  E& operator=( E const& )=default;
  E& operator=( E && )=default;
  explicit operator bool() const { return (bool)pImpl; }

  void init() { if (*this) pImpl->init(); }
  void insert(const K& k) ( if (*this) pImpl->insert(k); }
  size_t count() const { if (*this) pImpl->count(); else return 0; }
};

Our E<K> is now a value type. It can be stored in a vector , copied, moved, etc.

How do we do EOne and ETwo ?

First, take your existing EOne and ETwo and call them EOne_impl and ETwo_impl . Implement a clone() function that does a return std::make_unique<EOne_impl>(*this); for EOne_impl and similar for ETwo_impl .

Then this helper:

template<class Impl, class K>
struct E_impl: E<K> {
  using E<K>::E<K>;
  E_impl() : E<K>( std::make_unique<Impl>() ) {}
  template<class...Args>
  E_impl(Args&&...args) : E<K>( std::make_unique<Impl>(std::forward<Args>(args)...) ) {}
};

gives us

template<class K>
using Eone = E_impl< Eone_impl, K >;
template<class K>
using Etwo = E_impl< Etwo_impl, K >;

and I believe your J and main code starts compiling and working as-is.

What we just did was create value-semantics E<K> type that contains a pImpl (pointer to implementation) pointing to a pure-virtual interface that knows how to copy itself, as well as the interface we want on an E<K> .

We then forwarded the interface of E<K> to the E_interface<K> for each method. We didn't expose copy_from or clone , as those become operator= and our copy constructor.

To implement E<K> , you first implement E_interface<K> . Then I wrote a helper to create a derived type from E<K> that implicitly uses that implementation.

Note that our E<K> is almost-never empty; not never-empty. This is more efficient and simpler, but can cause problems down the road.

E<K> becomes a polymorphic value-semantics type. This is a strange beast in some senses (as many languages don't support such a type), but in other senses it behaves exactly the way you'd want it to behave.


A similar solution in C# or Java would have the data stored in the vectors be fully garbage collected reference-semantics types, not value-semantics types.

This is similar to a std::vector<std::shared_ptr<E<K>> (with the note that shared pointers are not fully garbage collected). Also note that copies of the shared_ptr end up being pointing to the same object, not new copies of it.

A std::vector<value_ptr<E_interface<K>> would also be a reasonable solution and get rid of some of the gymnastics I did in my E<K> and E_impl<K> . In this case, you wouldn't rename E to E_interface . You'd initialize the vector with

J<std::string> j(10, std::make_unique<EOne<std::string>>(e))

or somesuch.


Part of your problem is you have to ask yourself "what does it mean to copy an E<K> ". In C++ you get to answer this question yourself; depending on how you answer it, you may or may not be permitted to store it in a std::vector .

Since std::vector<E<K>> v; requires a static instantiation of class E, it will never compile (as you already noticed correctly). To make it work should use

std::vector<std::shared_ptr<E<K>>> v;

instead. It can store your objects EOne and ETwo dynamically while they can be referred with a pointed of type E . To add a new object to the vector you can use push_back:

v.push_back(std::make_shared<EOne<K>>{});

To cast between the types you can use dynamic and static cast functions for smart pointers eg std::dynamic_pointer_cast<>() .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM