首先,没有像“界面”这样的内置概念。通过C ++中的接口,我的意思是一些看起来像的抽象基类:
struct ITreeNode
{
... // some pure virtual functions
};
然后我们可以使用实现接口的具体结构,例如:
struct BinaryTreeNode : public ITreeNode
{
BinaryTreeNode* LeftChild;
BinaryTreeNode* RightChild;
// plus the overriden functions
};
这很有道理:ITreeNode
是一个界面;并非每个实施都有Left
& Right
个孩子 - 仅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;
};
这一切都很好,直到这一点: 我们现在假设我们需要编写一个BinaryTree类。
有一个函数插入,它接受一个值T并使用某种算法将其插入根节点(当然它将是递归的)。
为了使函数可测试,可模拟并遵循良好实践,参数需要是接口,而不是具体的类。 (让我们说这是一个严格的规则,不能被打破。)
template<typename T>
void BinaryTree<T>::Insert(const T& value, typename ITreeNode<T>::UniquePtr& ptr)
{
Insert(value, ptr->Left); // Boooooom, exploded
// ...
}
这是问题所在:
Left不是ITreeNode的字段!最糟糕的是,你无法将unique_ptr<Base>
投射到unique_ptr<Derived>
!
这样的场景的最佳做法是什么?
非常感谢!
答案 0 :(得分:4)
好的,过度工程了!但请注意,在大多数情况下,这种低级数据结构可以从透明度和简单的内存布局中获益。将抽象级别放在容器上方可以显着提升性能。
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;
}
};