【问题标题】:std::any without RTTI, how does it work?没有 RTTI 的 std::any,它是如何工作的?
【发布时间】:2018-12-23 23:43:18
【问题描述】:

如果我想使用std::any,我可以在关闭 RTTI 的情况下使用它。以下示例也可以使用-fno-rtti 和 gcc 按预期编译和运行。

int main()
{   
    std::any x;
    x=9.9;
    std::cout << std::any_cast<double>(x) << std::endl;
}

但是std::any 是如何存储类型信息的呢?如我所见,如果我使用“错误”类型调用std::any_cast,我会得到std::bad_any_cast 的预期异常。

这是如何实现的,或者这可能只是一个 gcc 功能?

我发现boost::any 也不需要 RTTI,但我也发现不是如何解决的。 Does boost::any need RTTI?.

深入研究 STL 标头本身并没有给我答案。该代码对我来说几乎无法阅读。

【问题讨论】:

  • Boost 有自己的 typeinfo 来代替 RTTI,这就是为什么 boost::any 不需要它。一般来说,除了实现自己的不依赖于 RTTI 的 typeinfo 之外,我没有看到其他可能性
  • any 有方法type() 返回一个type_info,它真的在没有rtti 的情况下运行吗?
  • @bipll:不,如果 RTTI 关闭,该功能将被关闭。所以在幕后,有些东西可以生成类似 typeid 的信息。但这似乎是实施的阴暗面;)

标签: c++ stl c++17 rtti


【解决方案1】:

TL;DR; std::any 持有一个指向模板类的静态成员函数的指针。该函数可以执行许多操作,并且特定于给定类型,因为函数的实际实例取决于类的模板参数。


std::any在libstdc++中的实现并不复杂,大家可以看看:

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/any

基本上,std::any 包含两件事:

  • 指向(动态)分配存储的指针;
  • 指向“存储管理器函数”的指针:
void (*_M_manager)(_Op, const any*, _Arg*);

当您使用T 类型的对象构造或分配新的std::any 时,_M_manager 指向特定于类型T 的函数(它实际上是特定于@987654332 的类的静态成员函数@):

template <typename _ValueType, 
          typename _Tp = _Decay<_ValueType>,
          typename _Mgr = _Manager<_Tp>, // <-- Class specific to T.
          __any_constructible_t<_Tp, _ValueType&&> = true,
          enable_if_t<!__is_in_place_type<_Tp>::value, bool> = true>
any(_ValueType&& __value)
  : _M_manager(&_Mgr::_S_manage) { /* ... */ }

由于此函数特定于给定类型,因此您不需要 RTTI 来执行std::any 所需的操作。

此外,很容易检查您在std::any_cast 中的类型是否正确。这里是std::any_cast的gcc实现的核心:

template<typename _Tp>
void* __any_caster(const any* __any) {
    if constexpr (is_copy_constructible_v<decay_t<_Tp>>) {
        if (__any->_M_manager == &any::_Manager<decay_t<_Tp>>::_S_manage) {
            any::_Arg __arg;
            __any->_M_manager(any::_Op_access, __any, &__arg);
            return __arg._M_obj;
        }
    }
    return nullptr;
}

您可以看到,它只是您尝试转换的对象内的存储函数 (_any-&gt;_M_manager) 和您要转换为的类型的管理器函数 (&amp;any::_Manager&lt;decay_t&lt;_Tp&gt;&gt;::_S_manage) 之间的相等性检查。


_Manager&lt;_Tp&gt; 类实际上是_Manager_internal&lt;_Tp&gt;_Manager_external&lt;_Tp&gt; 的别名,具体取决于_Tp。 该类还用于为std::any 类分配/构造对象。

【讨论】:

  • 简而言之:它们存储一个指向模板函数的静态实例的指针,该实例是唯一的,因为该模板的实例取决于给定的类型。我是对的?
  • @Klaus 总之,是的 ;)
  • 请注意,此构造(指向静态函数模板实例的指针对于每种模板类型都是唯一的)会因某些编译器优化而中断,例如 MSVC 的 /Gy 结合其链接器的 /OPT:ICF,请参阅注释在this page)。
  • 请注意,这取决于链接器和加载器合并函数的多个实例化,因此在 MinGW 上无法跨 DLL 边界运行 (stackoverflow.com/questions/45290296/…)
  • 虽然这是一个有用的答案,但您没有解决在any_cast 上如何比较类型是否相等的问题,因此可以在必要时抛出bad_any_cast。 OP 明确提到了这两点。 (似乎最简单的方法是将存储的函数指针与any_cast 的实例化预期的指针进行相等比较。)
【解决方案2】:

一种可能的解决方案是为可能存储在any 中的每种类型生成唯一的 id(我假设您更了解any 内部的工作原理)。可以做到这一点的代码可能如下所示:

struct id_gen{
    static int &i(){
        static int i = 0;
        return i;
    }

    template<class T>
    struct gen{
        static int id() {
            static int id = i()++;
            return id;
        }
    };    
};

有了这个实现,你可以使用类型的 id 而不是 RTTI typeinfo 来快速检查类型。

注意函数和静态函数中静态变量的使用。这样做是为了避免静态变量初始化顺序未定义的问题。

【讨论】:

  • 我认为你需要将 ig_gen::i 的初始化放入它自己的编译单元中
  • @RichardHodges 我真的不确定,也许你是对的。无论如何,因为这个概念证明表明没有 RTTI 是可能的。随意编辑我的答案
  • 看静态初始化顺序惨败。
  • @T.C.这不是确切的静态初始化顺序惨败的情况,但无论如何你是对的,静态初始化的顺序是未定义的。我改变答案。
【解决方案3】:

有限 RTTI 的手动实现并不难。您将需要静态通用函数。在不提供完整实现的情况下,我可以说这么多。 这是一种可能性:

class meta{
    static auto id(){
        static std::atomic<std::size_t> nextid{};
        return ++nextid;//globally unique
    };
    std::size_t mid=0;//per instance type id
public:
    template<typename T>
    meta(T&&){
        static const std::size_t tid{id()};//classwide unique
        mid=tid;
    };
    meta(meta const&)=default;
    meta(meta&&)=default;
    meta():mid{}{};
    template<typename T>
    auto is_a(T&& obj){return mid==meta{obj}.mid;};
};

这是我的第一个观察;远非理想,缺少许多细节。可以使用meta 的一个实例作为他假定的std::any 实现的非静态数据成员。

【讨论】:

  • 问题是:“这是如何实现的,或者这可能只是 gcc 功能?”它可以完成并不是一个答案,它可以完成是非常清楚的,因为提供的代码可以编译和工作!
  • 我确实提到了:静态泛型函数。但细节杀人。可以嵌入大量元数据,并且可以实现广泛的实现。真的很难选择答案的许多可能性之一。 OP能承受这么多吗?
  • 很好奇为什么这个有 -5,而另一个用看似更模糊和更少线程安全的术语说同样事情的答案有 +3...?
  • @underscore_d 感谢您的关注。原来错过了sn-p,因为懒得加了。那时是-3,应该停在那里。但这就是社会雪崩效应,人们只是互相跟随。我已经在评论中指出了更好的答案:接受的答案。并且真的不在乎选票。请阅读霍尔特的。
  • 我投了赞成票。这是一个很好的技术背景。由于不好的理由,这个答案被鲁莽地否决了。
猜你喜欢
  • 1970-01-01
  • 2014-09-08
  • 1970-01-01
  • 1970-01-01
  • 2013-06-15
  • 2012-12-15
  • 2017-10-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多