繁体   English   中英

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

[英]C++ avoid call to abstract base class's constructor

目前,我正在用 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的构造函数使用该引用来实例化v所有n对象,使用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++,但我相信我也会在其他编译器上得到同样的错误)。 因此,我的问题与如何克服这个问题有关? 你对我如何使我的设计更实用有什么建议吗?

谢谢

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


是时候学习如何使类型成为常规类型了。

常规类型的实例表现得像一个值。 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 实现一个执行return std::make_unique<EOne_impl>(*this);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))

或诸如此类。


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

由于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<>()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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