【问题标题】:Practically safe to assume sizeof(std::unordered_map<std::string, T>) is the same for all T?实际上可以安全地假设 sizeof(std::unordered_map<std::string, T>) 对于所有 T 都是相同的?
【发布时间】:2015-05-13 04:34:04
【问题描述】:

我处于两个类的定义之间存在循环依赖循环的情况,其中(据我所知)两个类都需要另一个类型是完整的类型才能正确定义它们。

简而言之,我需要的简化版是怎么回事:

struct Map;

struct Node {
    // some interface...
private:
    // this cannot be done because Map is an incomplete type
    char buffer[sizeof(Map)];
    // plus other stuff...
    void* dummy;
};

struct Map {
    // some interface...
private:
    // this is Map's only member
    std::unordered_map<std::string, Node> map_;
};

情况实际上比上述情况更复杂,因为Node 实际上将是一种变体类型(类似于boost::variant),它使用placement new 来显式构造预分配(和正确对齐,我在这个简化中忽略了)缓冲区:因此缓冲区不完全是sizeof(Map),而是一些依赖于sizeof(Map)的计算常数。

问题很明显,当Map 仅被前向声明时,sizeof(Map) 不可用。此外,如果我将声明的顺序更改为首先转发声明Node,那么Map 的编译将失败,因为当Node 是不完整的类型时,std::unordered_map&lt;std::string, Node&gt; 无法实例化,至少对于我的 GCC 4.8.2在 Ubuntu 上。 (我知道它比 GCC 版本更依赖于 libstdc++ 版本,但我不知道如何找到它......)

作为替代方案,我正在考虑以下解决方法:

struct Node {
    // some interface...
private:
    // doing this instead of depending on sizeof(Map)
    char buffer[sizeof(std::unordered_map<std::string, void*>)];
    // other stuff...
    void* dummy;
};

struct Map {
    // some interface...
private:
    // this is Map's only member
    std::unordered_map<std::string, Node> map_;
};

// and asserting this after the fact to make sure buffer is large enough
static_assert (sizeof(Map) <= sizeof(std::unordered_map<std::string, void*>),
    "Map is unexpectedly too large");

这基本上依赖于std::unordered_map&lt;std::string, T&gt; 对所有 T 的大小相同的假设,从我使用 GCC 的测试来看,这似乎是正确的。

因此,我的问题有三个:

  • C++ 标准中有什么要求这个假设成立的吗? (我假设没有,但如果有的话我会很惊喜......)

  • 如果不是,实际上是否可以安全地假设它适用于所有合理的实现,并且我的修订版本中的静态断言永远不会触发?

    李>
  • 最后,有没有我没想到的更好的解决方法?我敢肯定,有可能我可以做一些我没有想到的显而易见的事情,但不幸的是我什么都想不到......

【问题讨论】:

标签: c++ c++11 stl language-lawyer


【解决方案1】:

1) 没有

2) STL 容器不能用不完整的类型实例化。但是,显然有些编译器确实允许这样做。不允许这不是一个微不足道的决定,在许多情况下,您的假设确实成立。 This 文章可能会让您感兴趣。鉴于根据标准,如果不添加间接层,则无法解决此问题,并且您不想这样做。我只需要提醒一下你确实没有按标准做事。

话虽如此,我认为您的解决方案是使用 stl 容器的最佳解决方案。当大小确实超过预期大小时,静态断言确实会发出警告。

3) 是的,添加另一层间接,我的解决方案如下:

您遇到的问题是对象的大小取决于其数组的大小。假设你有一个对象 A 和一个对象 B:

struct A
{
   char sizeof[B]
}

struct B
{
    char sizeof[A]
}

对象 A 将增长,以便容纳 B 大小的字符。但反过来,对象 B 将不得不增长。我想你可以看到这是怎么回事。我知道这是您的确切问题,但我认为基本原则非常相似。

在这种特殊情况下,我会通过更改

char buffer[sizeof(Map)];

线只是一个指针:

char* buffer

并在初始化后动态分配内存。播种您的 cpp 文件将如下所示:

//node.cpp
//untested code
node::node()
{
    buffer = malloc(sizeof(map));
}

node::~node()
{
    free buffer;
}

【讨论】:

  • 不,Map包含std::unordered_map&lt;std::string, Node&gt;,而不是直接包含Node,而且前者实际上并没有随着Node的大小而增长,所以这种情况不像你说的情况.实际上,std::unordered_map&lt;std::string, Node&gt; 实际上并没有随着Node 的变化而改变大小,因为它不直接包含Node;任何根据value_type 参数改变大小的实现都必须进行一些奇怪的优化,我试图排除在实践中实际上发生的情况。
  • 另外,我不想像你说的那样动态分配内存,因为它增加了一个额外的间接层。有关更多解释,请参阅 cmets 中的讨论。
  • @Trantorian 正式地,任何使用类型的专业化声明都需要完整类型。 unordered_map 实际上并没有改变大小,这可能是真的。但我还是宁愿不去那里。这意味着您必须在没有完整类型的情况下实现任一类。这只是c++的一个要求,你不能声明两个依赖于彼此的完整类型的类。据我所知,恐怕解决这个问题的唯一方法确实是添加另一层间接性。
  • @Trantorian 另外,间接层并不是一件可怕的事情。至少在我看来,它比依赖似乎有效的代码要可怕得多。但不符合标准。
  • @Trantorian 哦,还有一件事。指针和数组之间的区别并没有那么大。在大多数情况下,它们可以被视为相同。
【解决方案2】:

继续假设。然后 static_assert 在构造上你是对的。

还有一些更高级的解决方案,例如弄清楚 boost 递归数据结构的工作原理并在此处应用该技术(这可能需要编写您自己的地图),或者仅使用支持不完整数据结构的 boost:: 容器。

【讨论】:

  • 我现在接受了这个,因为你是唯一一个不告诉我使用额外间接的人(这并不是真正解决问题的方法,只是避免它)。不过,如果您能详细说明递归数据结构,我会很好奇——我猜这是利用延迟模板名称绑定来允许具有相互依赖类型的结构的某种方式?
  • @tran 喜欢递归可选的工作方式。模板知道的某个标签引用了它自己。
  • 好的,谢谢,我想我明白了。也许类似的东西会有所帮助——我暂时保留static_assert 版本,并在后台考虑类似的东西作为替代
【解决方案3】:

1) 没有

2) 我不确定

3) 您也可以使用设计模式工厂方法。您的工厂将根据Map 变体返回对象(编辑:我的意思是您将使用 Map 变体实例作为参数,工厂方法实现将使用该信息相应地创建返回对象)并且它可以预先分配缓冲区以正确大小。

【讨论】:

  • 你能澄清你对 3) 的建议吗?特别是,我不希望通过指针进行比修订版中的额外间接(当static_assert 通过时有效)。 Node 实际上是一个联合,它包含多种类型中的一种而没有间接性,可能的类型列表包括 Mapstd::stringbool
  • 您的一个Node 实例将在生命周期中返回一种类型,还是会有所不同?你熟悉设计模式吗?
  • 我一次只有一种类型,但它可以改变。如果类型改变了,那么Node 将在当前内容上显式调用正确的类型析构函数并放置新的新类型的对象,在此过程中更新类型标记。我熟悉设计模式,但不明白您的意思——特别是,您似乎建议工厂将返回一些通用指针类型,例如 void* 以避免 Node 取决于 @ 的大小987654331@;这正是我不想要的,因为这是一个递归结构,额外的间接性会很快加起来。
  • 好吧,因为我看不到使用可变大小的 char 缓冲区而不是引用 Map 实例并从该实例获取数据的目的,我只能建议装饰器更改 Node
  • 因为您的建议添加了我绝对不想要的额外间接层。 char 缓冲区是因为我希望 Node 直接在其中存储对象:Node 可能包含 std::stringbool 或其他类型之一:想法是要有一个足够大的正确对齐的缓冲区持有最大的一个,实际上最终可能是Map。您的建议使递归迭代此结构的每一步都需要更多的间接性。
【解决方案4】:

1) 可能不会

2) 由于它看起来完全依赖于实现,这是一个很大的风险,因为这可能会在任何编译器更新时中断。

3) 您可以使用boost::unordered_map,它将接受不完整的类型,从而解决您的问题。

【讨论】:

    猜你喜欢
    • 2012-08-01
    • 2018-11-22
    • 1970-01-01
    • 2023-02-16
    • 2012-07-20
    • 2016-08-10
    • 1970-01-01
    • 2017-05-14
    • 2018-03-09
    相关资源
    最近更新 更多