获取模板以与 unique_ptr 一起很好地工作到 C++ 中的接口

Get template to work well with unique_ptr to interfaces in C++

首先,没有"interface"这样的内置概念。通过 C++ 中的接口,我的意思是一些抽象基础 class 看起来像:

struct ITreeNode
{
   ... // some pure virtual functions
};

然后我们可以有实现接口的具体结构,例如:

struct BinaryTreeNode : public ITreeNode
{
   BinaryTreeNode* LeftChild;
   BinaryTreeNode* RightChild;

// plus the overriden functions
};

很有道理:ITreeNode是一个接口;并非每个实现都有 LeftRight 子级 - 只有 BinaryTreeNode 有。

为了让东西可以广泛重用,我想写一个模板。所以 ITreeNode 需要是 ITreeNode<T>,而 BinaryTreeNode 需要是 BinaryTreeNode<T>,像这样:

template<typename T>
struct BinaryTreeNode : public ITreeNode<T>
{
};

为了让事情变得更好,让我们使用唯一指针(智能点更常见,但我知道解决方案 - dynamic_pointer_cast)。

template<typename T>
struct BinaryTreeNode : public ITreeNode<T>
{
   typedef std::shared_ptr<BinaryTreeNode<T>> SharedPtr;
   typedef std::unique_ptr<BinaryTreeNode<T>> UniquePtr;
   // ... other stuff
};

同样,

template<typename T>
struct ITreeNode
{
   typedef std::shared_ptr<ITreeNode<T>> SharedPtr;
   typedef std::unique_ptr<ITreeNode<T>> UniquePtr;
};

一切都很好,直到这一点: 假设现在我们需要写一个 class BinaryTree.

有一个函数insert,它接受一个值T,并使用某种算法将它插入到根节点中(自然会递归)。

为了使函数可测试、可模拟并遵循良好实践,参数需要是接口,而不是具体的 classes。 (假设这是一个不能被打破的硬性规则。)

template<typename T>
void BinaryTree<T>::Insert(const T& value, typename ITreeNode<T>::UniquePtr& ptr)
{
   Insert(value, ptr->Left); // Boooooom, exploded
   // ...
}

问题来了: 左边不是ITreeNode的字段!最糟糕的是,您不能将 unique_ptr<Base> 转换为 unique_ptr<Derived>!

对于这种情况,最佳做法是什么?

非常感谢!

好吧,过度工程化了!但请注意,在大多数情况下,这种低级数据结构极大地受益于透明性和简单的内存布局。将抽象级别置于容器之上可以显着提高性能。

template<class T>
struct ITreeNode {
  virtual void insert( T const & ) = 0;
  virtual void insert( T      && ) = 0;

  virtual T const* get() const = 0;
  virtual T      * get()       = 0;
  // etc
  virtual ~ITreeNode() {}
};

template<class T>
struct IBinaryTreeNode : ITreeNode<T> {
  virtual IBinaryTreeNode<T> const* left()  const = 0;
  virtual IBinaryTreeNode<T> const* right() const = 0;

  virtual std::unique_ptr<IBinaryTreeNode<T>>& left()  = 0;
  virtual std::unique_ptr<IBinaryTreeNode<T>>& right() = 0;

  virtual void replace(T const &) = 0;
  virtual void replace(T      &&) = 0;
};

template<class T>
struct BinaryTreeNode : IBinaryTreeNode<T> {
  // can be replaced to mock child creation:
  std::function<std::unique_ptr<IBinaryTreeNode<T>>()> factory
    = {[]{return std::make_unique<BinaryTreeNode<T>>();} };

  // left and right kids:
  std::unique_ptr<IBinaryTreeNode<T>> pleft;
  std::unique_ptr<IBinaryTreeNode<T>> pright;

  // data.  I'm allowing it to be empty:
  std::unique_ptr<T> data;

  template<class U>
  void insert_helper( U&& t ) {
    if (!get()) {
      replace(std::forward<U>(t));
    } else if (t < *get()) {
      if (!left()) left() = factory();
      assert(left());
      left()->insert(std::forward<U>(t));
    } else {
      if (!right()) right() = factory();
      assert(right());
      right()->insert(std::forward<U>(t));
    }
  }
  // not final methods, allowing for balancing:
  virtual void insert( T const&t ) override { // NOT final
    return insert_helper(t);
  }
  virtual void insert( T &&t ) override { // NOT final
    return insert_helper(std::move(t));
  }
  // can be empty, so returns pointers not references:
  T const* get() const override final {
    return data.get();
  }
  T      * get()       override final {
    return data.get();
  }
  // short, could probably skip:
  template<class U>
  void replace_helper( U&& t ) {
    data = std::make_unique<T>(std::forward<U>(t));
  }
  // only left as customization points if you want.
  // could do this directly:
  virtual void replace(T const & t) override final {
    replace_helper(t);
  }
  virtual void replace(T      && t) override final  {
    replace_helper(std::move(t));
  }
  // Returns pointers, because no business how we store it in a const
  // object:
  virtual IBinaryTreeNode<T> const* left()  const final override {
    return pleft.get();
  }
  virtual IBinaryTreeNode<T> const* right() const final override {
    return pright.get();
  }
  // returns references to storage, because can be replaced:
  // (could implement as getter/setter, but IBinaryTreeNode<T> is
  // "almost" an implementation class, some leaking is ok)
  virtual std::unique_ptr<IBinaryTreeNode<T>>& left() final override {
    return pleft;
  }
  virtual std::unique_ptr<IBinaryTreeNode<T>>& right() final override {
    return pright;
  }
};