【问题标题】:Incomplete types in collection iterators集合迭代器中的不完整类型
【发布时间】:2016-02-27 05:29:36
【问题描述】:

我为自己编写了一个自定义 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&lt;T,A&gt;::node 是我用来管理树的私有结构,它的成员中有T data;,但是如果T 不完整,则这是非法的。奇怪的是,当我改用std::list 时,没有这样的问题,我理解这是未定义的行为。

有没有解决这个问题的简单方法? avl_tree&lt;T,A&gt;::iterator 内部只维护一个指针node *ptr,因为指针具有固定大小,所以对于不完整类型来说这应该不是问题。但我不想将node 类暴露给public,我想使用iterators。尽管如此,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获取。

【问题讨论】:

  • 您的avl_tree 是否直接存储节点(不是指向它的指针)- 例如:对于根节点?更多代码 sn-ps 显示您的节点/树/迭代器的数据成员将不胜感激(可以省略方法 - 就像“结构概述”一样)。
  • 递归对象定义始终是我看到通过间接解决的问题,例如avl_tree&lt;vertex*&gt;::iterator partneravl_tree&lt;boost::recursive_wrapper&lt;vertex&gt;&gt;::iterator partner。后者只是简单地包装了一个动态分配的对象,为您返回值语义,该对象旨在准确处理此类情况。哪种解决方案最适合您取决于您​​,但基本上对于这样的递归数据结构,您需要间接。
  • 简单的方式 IMO 以极小的成本为根的效率:在那里添加间接以中断递归。存储node* root。它应该只产生可以忽略不计的差异,以换取整个容器的更大通用性。如果你测量它并发现它作为一个热点足够讨厌,你可以为整个树实现一个固定的分配,并有可能改进一切(链接结构可以从一个不太通用的分配中受益匪浅)。
  • @Ike 我确实最终将 root 更改为 node*。我无法衡量性能上的任何差异。由于树无论如何都存储在堆上,因此根处的附加间接性并没有受到伤害,它假设。
  • @Jonas 在根处切断链接是一个很好的开始策略——如果你想要更快的速度,可以关注分配器。这也是std::list 在这种上下文中可以避免递归类型依赖的原因:它存储指向尾部和头部的指针,因此不需要节点的完整类型定义(同样因此不需要T) 在实例化时的完整类型定义。与std::map 类似,它通常存储一个指针。

标签: c++ templates stl incomplete-type


【解决方案1】:

我想nodeiterator 类型没有什么问题。问题是你使用递归类型定义

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_treeBnodeCiterator。而且错误是一样的

错误:“A::B::data”的类型不完整

现在有多种方法可以修复它。第一个是将D类型中ad的类型更改为

A<D*>::C ad;

但是,正如您所提到的,STL 的list(或vector)没有这样的问题。这是交易,A 中的root 的类型应该是B*B&amp;,而不是B。但如果您使用B*,则需要注意内存分配。

【讨论】:

  • 这确实成功了。但是,我仍然不能 100% 理解为什么 root 会出现问题。你说:“所以在node root 的实例化时出现错误,编译器不知道T 的大小。”但是在那个时候(定义顶点的地方)我只使用avl_tree&lt;T,A&gt;::iterator,它不会直接实例化node或通过avl_tree,而是只保留一个指针node*,这应该没问题。
  • @Jonas 原因是iterator的定义使用了avl_tree的定义。不需要avl_tree 的实例,但是编译器应该知道avl_tree 类型的大小并计算它需要vertex 的大小。
【解决方案2】:

完整类型

首先,让我们看一个定义 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 信息的事情,特别是,除非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->...

除此之外,我们还有先有鸡还是先有蛋的问题。如果FooNode 之前,则Node 在定义Foo 时不可能定义,如果NodeFoo 之前定义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 不依赖于TreeNode 的定义,这将编译得很好。

尽管如此,在您的情况下,Tvertex,它取决于存储节点的树的类型定义,该节点存储顶点。结果,我们就有了这样的场景:

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 的成员,即使它只是一个类型名,这需要编译器具有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;
};

这里的相关类型依赖如下所示:

list<T>->POD
node->T->...

打破类型依赖

从上面我们可以看出,我们可以通过指针引入间接来切断/打破类型依赖。这样做,我们不再需要用户定义的类型定义,而是可以将 UDT 依赖更改为简单的 POD 依赖。

间接可以放置在您喜欢的任何位置,但对于链接结构,通常最方便的位置是结构的root/head/tail。这可以防止使用您的链接结构的客户端不必担心这些递归/循环类型依赖关系。

间接成本

我经常听到的一件事是“间接成本”,好像这非常昂贵。这完全取决于内存访问模式以及它们与从寄存器到二级内存分页的内存层次结构的关系。虽然将其视为“间接成本”是一种简单而通用的看待它的方式,因为指针可以指向内存中的所有位置,但真正重要的是我们如何访问内存尊重这些指针。

如果我们在一个连续的内存空间中顺序遍历它,即使是一个链表也可以非常有效,其中多个节点适合一个缓存行并在驱逐之前被访问。它们通常不那么快的地方是因为节点通常是由通用分配器分配的,而不是一次性分配的,这会在内存空间中分散和分割它们的内容,并在遍历期间导致缓存未命中。这里最大的不同是内存布局。

因此,如果您担心破坏类型依赖所需的间接成本,请不要担心。除非在非常精细的情况下仅与指针的内存大小有关,否则通常需要担心的是错误的事情。而是查看内存分配的方式,寻找引用的位置。使用正确的内存分配策略,即使是不可避免地依赖大量间接的链接结构也可以变得非常高效。

【讨论】:

  • 很棒的答案!非常感谢您抽出宝贵的时间,非常感谢。我有点想念的是将avl_tree&lt;T&gt;::iterator 视为访问成员,只是typename 类型之一。这可以解释为什么编译器需要在那个时候弄清楚avl_tree&lt;T&gt; 的匹配。也感谢有关间接成本的详细信息。我将尝试编写一个自定义分配器并运行一些测试以获得乐趣。
  • @Jonas 如果你真的对内存分配器方面感兴趣,我在这里包括了一个(见底部——无耻插件):stackoverflow.com/questions/31580869/…。它显示了一个基本的 O(1) 固定分配器。我用它来加速跳过列表节点的分配,但那里的代码应该适用于任何事情(class FixedAlloc)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-10
  • 2010-10-23
  • 2023-02-02
  • 2015-11-28
  • 1970-01-01
相关资源
最近更新 更多