没有运行时类型信息的 std::any,它是如何工作的?

46
如果我想要使用 std::any,我可以在关闭RTTI的情况下使用它。下面的示例在使用gcc时也可正常编译并运行 -fno-rtti
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 头文件本身并没有给我答案。对我来说,那段代码几乎无法阅读。


2
Boost有自己的typeinfo来替代RTTI,这就是为什么boost::any不需要它的原因。一般来说,我没有看到其他可能性,除了实现自己的typeinfo,而这个typeinfo不依赖于RTTI。 - bartop
any 有一个 type() 方法,返回一个 type_info,它真的可以在没有 RTTI 的情况下运行吗? - bipll
1
@bipll:不,如果关闭了RTTI,确切地说就是该函数被关闭了。因此,在底层,有一些东西可以生成类似于typeid的信息。但这似乎是实现的黑暗面;) - Klaus
1
请找到以下 Boost type_info 的源代码: https://github.com/boostorg/core/blob/develop/include/boost/core/typeinfo.hpp 祝愉快 :) - Richard Hodges
3个回答

60

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


在libstdc++中实现std::any并不是很复杂,你可以看一下:

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的函数(实际上是特定于T的类的静态成员函数):

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->_M_manager) 和你想要转换的类型的管理函数 (&any::_Manager<decay_t<_Tp>>::_S_manage) 是否相等。
_Manager<_Tp> 实际上是别名,根据 _Tp 的不同,它可以是 _Manager_internal<_Tp>_Manager_external<_Tp>。 该类还用于为 std::any 类分配/构造对象。

10
简而言之:它们存储一个指针,指向一个静态的模板函数实例,这个实例是独特的,因为该模板的实例取决于给定的类型。我的理解正确吗? - Klaus
1
@Klaus 简而言之,是的 ;) - Holt
4
注意,这个构造(每个模板类型的静态函数模板实例的指针是唯一的)会因某些编译器优化而破坏,例如MSVC的/Gy与其链接器的/OPT:ICF结合使用时,请参见此页面上的说明。 - rubenvb
4
请注意,这取决于链接器和装载器合并函数的多个实例,因此在MinGW跨DLL边界上无法正常工作(参见https://dev59.com/MaPia4cB1Zd3GeqP2rj1)。 - T.C.
1
虽然这是一个有用的答案,但您没有解决如何在any_cast时比较类型以进行相等性比较的问题,因此必要时可以抛出bad_any_cast异常。OP明确提到了这两个问题。(最简单的方法似乎是将存储的函数指针与从any_cast实例化中期望的函数指针进行相等性比较。) - Arne Vogel
显示剩余10条评论

7

手动实现有限的运行时类型信息(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实现的非静态数据成员之一。

2
问题是:“这是如何实现的,还是这只是gcc的一个特性?” 仅仅说它可以做到并不是一个答案,而且由于提供的代码可以编译和工作,所以这一点是非常清楚的! - Klaus
我提到了如何使用静态泛型函数,但细节决定成败。可以嵌入大量元数据,并且有多种实现方式可供选择。很难从众多可能性中选择一个答案。提问者能承受这么多吗? - Red.Wave
2
为什么这个回答得到了-5的评分,而另一个表达方式似乎更加模糊、不太线程安全的回答却得到了+3的评分呢? - underscore_d
4
@underscore_d,感谢您的关注。最初我错过了这个片段,因为我不想添加它。当时它是-3分,本应该停在那里。但这就是社交雪崩效应,人们只是互相跟随。我已经在评论中指出了更好的答案:被接受的答案。而且我并不真的在意得票数。请阅读霍尔特的回答。 - Red.Wave
2
我点了赞。这是一个很好的技术背景。这个答案因为没有好的理由被鲁莽地投了反对票。 - Xofo

4

可能的解决方案之一是为每个可能存储在any中的类型生成唯一标识符(我假设您大致了解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 的初始化放入自己的编译单元中。 - Richard Hodges
@RichardHodges 我真的不确定,也许你是对的。无论如何,正如这个概念验证所示,没有运行时类型信息也是可能的。请随意编辑我的回答。 - bartop
3
请参阅静态初始化顺序问题。 - T.C.
@T.C. 这不是确切的静态初始化顺序混乱情况,但无论如何您是正确的,静态初始化的顺序是未定义的。我改变了答案。 - bartop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接