簡體   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