获取模板以与 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
是一个接口;并非每个实现都有 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;
};
一切都很好,直到这一点:
假设现在我们需要写一个 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;
}
};
首先,没有"interface"这样的内置概念。通过 C++ 中的接口,我的意思是一些抽象基础 class 看起来像:
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;
};
一切都很好,直到这一点: 假设现在我们需要写一个 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;
}
};