【问题标题】:Why does libc++'s implementation of map use this union?为什么 libc++ 的 map 实现要使用这个 union?
【发布时间】:2015-07-25 05:51:00
【问题描述】:
#if __cplusplus >= 201103L

template <class _Key, class _Tp>
union __value_type
{
    typedef _Key                                     key_type;
    typedef _Tp                                      mapped_type;
    typedef pair<const key_type, mapped_type>        value_type;
    typedef pair<key_type, mapped_type>              __nc_value_type;

    value_type __cc;
    __nc_value_type __nc;

    template <class ..._Args>
    _LIBCPP_INLINE_VISIBILITY
    __value_type(_Args&& ...__args)
        : __cc(std::forward<_Args>(__args)...) {}

    _LIBCPP_INLINE_VISIBILITY
    __value_type(const __value_type& __v)
        : __cc(__v.__cc) {}

    _LIBCPP_INLINE_VISIBILITY
    __value_type(__value_type& __v)
        : __cc(__v.__cc) {}

    _LIBCPP_INLINE_VISIBILITY
    __value_type(__value_type&& __v)
        : __nc(std::move(__v.__nc)) {}

    _LIBCPP_INLINE_VISIBILITY
    __value_type& operator=(const __value_type& __v)
        {__nc = __v.__cc; return *this;}

    _LIBCPP_INLINE_VISIBILITY
    __value_type& operator=(__value_type&& __v)
        {__nc = std::move(__v.__nc); return *this;}

    _LIBCPP_INLINE_VISIBILITY
    ~__value_type() {__cc.~value_type();}
};

#else
// definition for C++03...

看起来目的是使__value_type 可分配和可移动,同时还能够将内容公开为pair&lt;const key_type, mapped_type&gt;(这是迭代器的值类型等)。但我不明白为什么它需要可分配或可移动,因为我看不出实现需要复制或移动地图内的节点的任何理由,或者实际上除了构造和销毁它们之外的任何事情-放置并重新配置指针。

【问题讨论】:

  • 您是否尝试在文件中搜索__nc 的用途?请注意,无论哪种方式,此联合建议的用法都不符合要求。
  • @Potatoswatter 是的,它只用于实现显示的成员。
  • 这可能是核心语言、库或两者都有缺陷。通常,该库应该可以使用该语言实现。您要报告问题吗?
  • @Potatoswatter 实现允许使用“魔法”
  • @MattMcNabb 标准库的神奇部分大部分都保留在 §18 中。委员会的意图是普通类型的数据结构不需要魔法。同样,libc++ 是与 Clang 不同的项目,许多其他标准库至少在某种程度上是可移植的。

标签: c++ c++11 dictionary stl libc++


【解决方案1】:

这是为了支持Potatoswatter 的回答。我作为这个 libc++ 代码的作者回答。

考虑:

int
main()
{
    std::map<A, int> m1;
    m1[A{1}] = 1;
    m1[A{2}] = 2;
    m1[A{3}] = 3;
    std::map<A, int> m2;
    m2[A{4}] = 4;
    m2[A{5}] = 5;
    m2[A{6}] = 6;
    std::cout << "start copy assignment\n";
    m2 = m1;
    std::cout << "end copy assignment\n";
}

在这种特殊情况下,我预见到需要回收映射的节点,并重新分配“const”键以使节点的回收有效。因此

http://cplusplus.github.io/LWG/lwg-defects.html#704

插入以下措辞以允许回收map 节点:

关联容器满足 分配器感知容器(23.2.1 [container.requirements.general]), 除了容器地图和多地图,要求放在 表 93 中的 value_type 直接应用于 key_type 和 映射类型。 [注意:例如 key_type 和 mapped_type 有时是 即使 value_type (pair) 不是 CopyAssignable,也必须是 CopyAssignable。 ——尾注]

因此允许容器非常量访问映射的 key_type。迄今为止,只有 libc++ 利用了这一点。如果您在上面的示例中检测 A,您将获得 libc++:

start copy assignment
operator=(const A& a)
operator=(const A& a)
operator=(const A& a)
end copy assignment

对于 libstdc++ (gcc-5.2.0)

start copy assignment
~A()
A(A const& a)
~A()
A(A const& a)
~A()
A(A const& a)
end copy assignment

对于 VS-2015:

start copy assignment
~A()
~A()
~A()
A(A const& a)
A(A const& a)
A(A const& a)
end copy assignment

我断言,当A 是诸如int、std::vector 或std::string 之类的类型,或包含这些常见std 类型之一的类型时,分配比销毁后跟的破坏具有巨大的性能优势建造。分配可以利用 lhs 中的现有容量,通常会导致简单的memcpy,而不是先释放内存,然后再分配内存。

请注意,上面的~A() 可能意味着释放整个节点,而不仅仅是A(尽管不一定)。在任何情况下,libc++ map 复制赋值运算符都经过高度优化以回收内存,并且该优化的权限得到 C++11 及更高标准的支持。联合技巧是实现该优化的一种(不一定是可移植的)方法。

当分配器不传播移动赋值并且两个分配器比较不相等时,移动赋值运算符的相同代码获得了类似的优化:

clang/libc++:

start move assignment
operator=(A&& a)
operator=(A&& a)
operator=(A&& a)
end move assignment

gcc-5.2.0

start move assignment
~A()
A(A const& a)
~A()
A(A const& a)
~A()
A(A const& a)
~A()
~A()
~A()
end move assignment

VS-2015

start move assignment
~A()
~A()
~A()
A(A const& a)
A(A const& a)
A(A const& a)
end move assignment

【讨论】:

  • "请注意,以上~A() 可能意味着释放整个节点,而不仅仅是A" 不适用于libstdc++。我们在分配时为节点回收内存,但我们在该位置销毁旧对象并构造一个新对象,因此我们避免了一级重新分配(但如果A 分配它自己的内存,我们仍然释放所有这些并重新分配当构造一个新的A)。
【解决方案2】:

当您使用自定义分配器时,可能需要将地图(及其内容)移动到新的资源池中。在这种情况下,此重载将提供对键的可移动访问:

__value_type(__value_type&& __v)
    : __nc(std::move(__v.__nc)) {}

键已被移出并不重要,因为接下来发生的事情是释放所有节点。

注意,这种用法可能会导致未定义的行为。你通常不能写一个工会的成员,然后读另一个。 Clang 和 libc++ 可以做到这一点,只要它们能在内部保证不会导致问题(或错误诊断)。

不过,他们可能是这样做的,因为没有很好的符合要求的替代方案。至少,我想不出一个。该标准要求 value_type::first_type 是真正的 const 合格,因此即使是 const_cast 也是不允许的。

在key_type 和mapped_type 都是标准布局的情况下,诀窍是一致的,因此std::pair&lt;key_type, mapped_type&gt; 和std::pair&lt;key_type const, mapped_type&gt; 是布局兼容的,根据[class.mem] §9.2/16。这里看起来有点奇怪,因为该函数引用联合的直接成员__cc 和__nc,让构造函数访问包含first 和second 的公共子序列。标准布局类型的 requirements 有点限制,但许多常见的键和值类型(例如,std::string)可能会满足它们。

【讨论】:

  • 这不符合要求。 9.2/16 仅适用于标准布局结构,K 和 V 不一定是标准布局。此外,您只能读取常见的初始序列,但移动通常需要写入。
  • @T.C. - 另一方面,允许标准库实现使用其编译器已明确定义的结构(作为扩展,可能未记录)。
  • @T.C.谢谢,我恢复了原来的段落并修改了。我看到的关于阅读和写作的语言是“允许检查”,这就像泥巴一样。
  • @T.C.谢谢,我找了类似的东西,但没有找到。该 DR 提到了“涉及 std::pair 等的特定用例”,这使得它看起来只是针对这个实现问题量身定制的。如果它没有走得足够远以实际支持它,那就太奇怪了。不过,我想,std::map 键的怪异也会给其他人带来问题。
  • @Potatoswatter 我指的是决议中的第 4 项,它澄清了“检查”的意思是“阅读”。无论如何,密钥被构造为const,并且有一条总括规则规定尝试修改const 对象是UB。
猜你喜欢
  • 2014-01-23
  • 1970-01-01
  • 2015-07-11
  • 2021-11-08
  • 2012-04-29
  • 2019-03-26
  • 2012-06-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多