C ++避免调用抽象基类的构造函数

时间:2017-02-08 15:13:35

标签: c++ c++11

目前,我正在使用C ++(使用C ++ 11标准)构建一个库,我一直在试图弄清楚如何使我的设计更实用。我有以下抽象类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;
};

我想限制用户实例化它(即,作为一个接口)。 E有两个实现相应方法的子类:

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
};

ETwo : public E<K>,类似于EOne。此外,还有一个不同的类J,其成员std::vector<E<K>>需要在构造期间实例化:

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

本质上,通过获取E<K>对象的常量引用,我希望J的构造函数使用该引用来实例化n的所有v个对象e作为模板(即调用复制构造函数)。您可以想象,我的目标是让e成为EOne<K>ETwo<K>的对象。例如,我会通过以下方式拨打J<K>::J(size_t n, const E<K>& e)

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

但是,上面没有编译,编译器抱怨我无法实例化抽象类(我使用的是vc ++,但我相信我也会在其他编译器上得到同样的错误)。因此,我的问题必须在于如何克服这个问题?您对如何使我的设计更实用有任何建议吗?

谢谢

2 个答案:

答案 0 :(得分:1)

这方法不止一种。以下是最复杂的合理方法。它需要在类型定义中进行大量工作,但会导致最干净的客户端#34;使用这些类型的代码。

是时候学习如何定型了。

常规类型的实例表现得像一个值。与常规类型相比,C ++算法和容器的工作效果要好于抽象类型。

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;
};

这基本上是您的E,除了我添加了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;
  }
};

clone_ptr是一个智能指针,它是unique_ptr,知道如何通过调用存储对象上的clone()copy_from来复制自身。它可能有一些错别字。

现在我们写下我们的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; }
};

我们的E<K>现在是一种值类型。它可以存储在vector,复制,移动等

我们如何EOneETwo

首先,抓住现有的EOneETwo,并将其称为EOne_implETwo_impl。实施clone()函数,为return std::make_unique<EOne_impl>(*this);执行EOne_impl,为ETwo_impl实现类似功能。

然后这个助手:

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)...) ) {}
};

给我们

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

我相信您的Jmain代码会开始编译并按原样运行。

我们刚刚创建的是创建值语义E<K>类型,其中包含一个pImpl(指向实现的指针),指向一个知道如何复制自身的纯虚拟接口,以及我们想要的接口。 E<K>

然后,我们为每种方法将E<K>的界面转发给E_interface<K>。我们没有公开copy_fromclone,因为它们变成operator=和我们的复制构造函数。

要实施E<K>,首先要实施E_interface<K>。然后我编写了一个帮助器来创建E<K>的派生类型,隐式使用该实现。

请注意,我们的E<K>几乎从不为空;永远不会空虚。这样更有效,更简单,但可能会导致问题。

E<K>成为多态值 - 语义类型。从某种意义上来说,这是一种奇怪的野兽(因为许多语言不支持这种类型),但在其他方面,它的行为方式与你希望它的行为方式完全相同。

C#或Java中的类似解决方案将存储在向量中的数据完全是垃圾收集的引用语义类型,而不是值语义类型。

这与std::vector<std::shared_ptr<E<K>>类似(注意共享指针未完全垃圾收集)。另请注意,shared_ptr的副本最终会指向同一个对象,而不是它的新副本。

std::vector<value_ptr<E_interface<K>>也是一个合理的解决方案,摆脱我在E<K>E_impl<K>中所做的一些体操。在这种情况下,您不会将E重命名为E_interface。您已使用

初始化vector
J<std::string> j(10, std::make_unique<EOne<std::string>>(e))

或某些。

你的部分问题是你必须问自己&#34;复制E<K>&#34;是什么意思。在C ++中,你可以自己回答这个问题;根据您的回答方式,您可能会或可能不会将其存储在std::vector

答案 1 :(得分:0)

由于std::vector<E<K>> v;需要E类的静态实例化,因此它永远不会编译(正如您已经正确注意到的那样)。要使它工作应该使用

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

代替。它可以动态存储您的对象EOneETwo,同时可以使用尖头类型E引用它们。要向矢量添加新对象,可以使用push_back:

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

要在类型之间进行转换,您可以为智能指针使用动态和静态转换函数,例如: std::dynamic_pointer_cast<>()