我自己写了一个自定义STL样式的容器,它在内部使用AVL树来组织数据。现在,在一个项目中,我希望它有一个迭代器作为成员:
class vertex {
...
avl_tree<vertex>::iterator partner;
...
}
然而,我收到错误:
error: ‘avl_tree<T, A>::node::data’ has incomplete type
T data;
^
从我在SO和其他网站上阅读的内容来看,vertex
在完全定义之前是不完整的类型。 avl_tree<T,A>::node
是我用来管理树的私有结构,它的成员中有T data;
,但如果T
不完整,则非法。奇怪的是,当我使用std::list
时,没有这样的问题,我理解这是一个未定义的行为。
有解决这个问题的简单方法吗? avl_tree<T,A>::iterator
内部只维护一个指针node *ptr
,对于不完整的类型,这不应该是一个问题,因为指针有固定的大小。但我不想将node
课程公开给public
,我想使用iterator
。尽管如此,无论templete参数如何,iterator
总是具有相同的大小,那么是否有办法强制编译器确认这一事实?
结构概述:
template <typename T, typename A = std::allocator<T> >
class avl_tree {
private:
class node {
public:
T data;
avl_tree *tree;
short depth;
size_type n;
node *parent;
node *left_child;
node *right_child;
};
public:
class iterator {
private:
node *ptr;
};
private:
using NodeAlloc = typename std::allocator_traits<A>::template rebind_alloc<node>;
NodeAlloc alloc;
node root;
};
完整代码可在GitHub上找到。
答案 0 :(得分:2)
我猜Vector::distance()
或node
类型没有任何问题。问题是您使用递归类型定义
iterator
您正在尝试使用尚未完全定义的类型(class vertex {
...
avl_tree<vertex>::iterator partner;
...
}
)。因此,在vertex
的实例化中,您遇到了错误,编译器不知道node root
的大小。
以下是模拟问题的小例子
T
template<typename T>
struct A {
struct B {
T data;
};
struct C {
B* b;
};
B root;
};
struct D {
A<D>::C ad;
};
int main() {
D d;
}
是A
,avl_tree
是B
,node
是C
。并且错误是相同的
错误:'A :: B :: data'的类型不完整
现在有多种方法可以修复它。第一个是将iterator
类型中ad
的类型更改为
D
但是正如您所提到的,STL(或A<D*>::C ad;
)的list
没有这样的问题。以下是此交易,vector
中的root
类型应为A
或B*
,而不是B&
。但是如果你使用B
,你将需要处理内存分配。
答案 1 :(得分:1)
首先,让我们看一个简单的例子,说明定义UDT(用户定义的类型)所需的内容。
struct Foo
{
struct Bar bar;
};
鉴于上述代码,只有了解struct Bar
的定义,编译器才能正确构建它。否则,它无法知道如何对齐此bar
数据成员以及结构的实际大小(以及要添加多少填充以确保其正确对齐)。
因此,为了能够以这种方式定义Foo
,同样需要Bar
。类型依赖关系如下所示:
Foo->Bar
如果我们将上面的代码更改为:
struct Foo
{
struct Bar* bar;
};
......突然间,我们正在寻找一个非常不同的场景。在这种情况下,Bar
被允许为不完整类型(已声明但未定义),因为Foo
仅存储指向它的指针。指针实际上是POD(普通旧数据类型)。无论指向Bar
还是Baz
,其大小和对齐要求都不会有所不同。因此,这里的类型依赖基本上是:
Foo->POD
因此,即使Bar
的定义未知,我们也可以编译此代码。当然,如果编译器遇到试图访问Bar
成员或构建它或执行任何需要Bar
信息的代码的代码,具体来说,除非{{1}的定义,否则会产生错误那时候可用。
让我们看一个递归类型依赖的简单示例:
Bar
对于这种情况,为了正确定义struct Foo
{
struct Foo next;
};
,我们必须正确定义Foo
。哎呀 - 无限递归。即使这是以某种方式允许的,系统也希望为Foo
分配无限量的内存。在这种情况下,类型依赖关系如下所示:
Foo
即使我们在中间引入了新类型,同样的问题仍然存在:
Foo->Foo->Foo->...
由于类型依赖关系的循环特性,我们仍然会遇到编译器错误,如下所示:
struct Foo
{
struct Node next;
};
struct Node
{
struct Foo element;
};
除此之外,我们还有鸡或蛋的问题。如果Foo->Node->Foo->Node->Foo->...
位于Node
之前,则Foo
无法定义Foo
,Node
无法在Node
时定义Foo
如果Node
位于Foo
之前,则会定义。
为了打破这个循环,我们可以添加一个间接:
struct Foo
{
struct Node* next;
};
struct Node
{
struct Foo element;
};
现在我们有:
Foo->POD
Node->Foo->POD
...这是有效的,避免循环类型依赖,并编译得很好。
更接近您的树示例,让我们看一下这样的案例:
template <class T>
struct Tree
{
struct Node
{
T element;
};
Node root;
};
在这种情况下,类型依赖关系如下所示:
Tree->Node->T->...
如果T
不依赖于Tree
或Node
的定义,这将编译正常。
然而,在您的情况下,T
是vertex
,它取决于存储存储顶点的节点的树的类型定义。因此,我们有这种情况:
avl_tree<vertex>->node->vertex->avl_tree<vertex>->node->vertex->...
...因此我们再次具有该循环类型依赖性。切断此依赖关系的最简单方法之一,也许是像这样的链接结构最常用的方法,是将root/head/tail
存储为指针。
template <class T>
struct Tree
{
struct Node
{
T element;
};
Node* root;
};
有了这个,我们就像这样切断了类型依赖:
Tree->POD
Node->T->...
......或者,根据你的例子进行调整:
avl_tree<vertex>->POD
node->vertex->avl_tree<vertex>->POD
......这完全没问题,打破了这个循环。
您可能想知道为什么从概念上讲,这需要完整的avl_tree
类型定义:
avl_tree<vertex>::iterator partner;
这里的迭代器很好,因为它存储了一个指向POD节点的指针。然而,这里的问题是我们正在尝试访问avl_tree
的成员,即使它只是一个类型名称,这要求编译器具有{{1}的完整类型定义(它并没有在我们可能喜欢的理想粒度级别上工作)。这要求avl_tree
的完整定义,然后需要node
的完整定义。
奇怪的是,当我使用
vertex
代替时,没有这样的问题
这是因为std::list
通常看起来像这样(给予或采取一些微小的变化):
std::list
此处的相关类型依赖关系如下所示:
template <class T, ...>
class list
{
public:
...
private:
struct node
{
node* next;
node* prev;
T element;
};
...
node* head;
node* tail;
};
从上面我们可以看到,我们可以通过指针引入间接来切断/破坏类型依赖。在这样做时,我们不再需要用户定义的类型定义,而是可以将UDT依赖项更改为简单的POD依赖项。
间接可以放在你喜欢的任何地方,但是对于链接结构,通常最方便的地方是结构的list<T>->POD
node->T->...
。这可以防止使用链接结构的客户端担心这些递归/循环类型依赖性。
我听到的很多事情之一就是&#34;间接成本&#34;,好像这是非常昂贵的。这一切都取决于内存访问模式以及它们与内存层次结构的关系,这些内存层次结构从寄存器一直到第二阶段内存中的分页。虽然将此视为间接成本&#34;是一种简单而通用的方式来查看它,因为指针可以指向内存中的所有位置,真正有用的是我们如何通过引用这些指针来访问内存。
如果我们在连续的内存空间中顺序遍历链接列表,即使链接列表也非常有效,其中多个节点适合高速缓存行并且在逐出之前被访问。它们通常不那么快的地方是由于节点通常由通用分配器分配而不是一次全部分配,在内存空间中散布和分段它们的内容并导致遍历期间的高速缓存未命中。它的内存布局在这里产生了最大的不同。
因此,如果您一直担心打破类型依赖所需的间接成本,请不要这样做。除非它只是在非常精细的情况下指针的内存大小,否则担心通常是错误的。而是查看内存的分配方式,寻找引用的位置。使用正确的内存分配策略,即使是不可避免地依赖大量间接的链接结构也会变得非常有效。