将函数模板中的静态局部variables的地址用作types标识符是否安全?

我希望创build一个替代std::type_index不需要RTTI :

 template <typename T> int* type_id() { static int x; return &x; } 

请注意,局部variablesx的地址被用作typesID,而不是x本身的值。 另外,我不打算在现实中使用裸指针。 我刚刚清除了与我的问题无关的所有内容。 看到我的实际type_index实施在这里 。

这种方法是否合理?如果是这样,为什么? 如果没有,为什么不呢? 我觉得我在这里摇摇欲坠,所以我对我的方法为什么会起作用或不起作用的确切原因感兴趣。

一个典型的用例可能是在运行时注册例程以通过单个接口处理不同types的对象:

 class processor { public: template <typename T, typename Handler> void register_handler(Handler handler) { handlers[type_id<T>()] = [handler](void const* v) { handler(*static_cast<T const*>(v)); }; } template <typename T> void process(T const& t) { auto it = handlers.find(type_id<T>()); if (it != handlers.end()) { it->second(&t); } else { throw std::runtime_error("handler not registered"); } } private: std::map<int*, std::function<void (void const*)>> handlers; }; 

这个类可能是这样使用的:

 processor p; p.register_handler<int>([](int const& i) { std::cout << "int: " << i << "\n"; }); p.register_handler<float>([](float const& f) { std::cout << "float: " << f << "\n"; }); try { p.process(42); p.process(3.14f); p.process(true); } catch (std::runtime_error& ex) { std::cout << "error: " << ex.what() << "\n"; } 

结论

感谢大家的帮助。 我已经接受@StoryTeller的答案,因为他已经概述了为什么解决scheme应该按照C ++规则有效。 然而,@SergeBallesta和评论中的其他人已经指出,MSVC执行的优化很难接近打破这种方法。 如果需要更强大的方法,那么使用std::atomic的解决scheme可能更可取,正如@galinette所build议的那样:

 std::atomic_size_t type_id_counter = 0; template <typename T> std::size_t type_id() { static std::size_t const x = type_id_counter++; return x; } 

如果有人有进一步的想法或信息,我仍然渴望听到!

是的,这在一定程度上是正确的。 模板函数隐式inlineinline函数中的静态对象在所有翻译单元之间共享。

因此,在每个翻译单元中,您将获得对type_id<Type>()的调用的相同静态局部variables的地址。 您在这里受到标准的ODR违规保护。

因此,本地静态地址可以作为一种自制的运行时types标识符。

这与标准是一致的,因为C ++使用模板而不是像Java一样的types擦除的generics,所以每个声明的types都会有自己的函数实现,包含一个静态variables。 所有这些variables是不同的,因此应该有不同的地址。

问题是,他们的价值永远不会被使用,更糟的是永远不会改变。 我记得优化器可以合并string常量。 由于优化器尽力比任何人类程序员都聪明得多,所以我会担心一个太热情的优化编译器会发现,由于这些variables值永远不会改变,所以它们都将保持一个0值,为什么不把它们全部合并到节省内存?

我知道,因为似乎规则,编译器可以自由地做它想要的,只要可观察的结果是相同的。 而且我不确定静态variables的地址总是会有相同的值是不一样的。 也许有人可以确认标准的哪一部分实际上关心它?

目前的编译器仍然分别编译程序单元,所以不能确定另一个程序单元是使用还是改变这个值。 所以我的意见是,优化器将没有足够的信息来决定合并variables,你的模式是安全的。

但是,我真的不认为这个标准可以保护它,所以我不能说C ++构build器(编译器+链接器)的未来版本是不是会发明一个全局优化阶段来主动search可以合并的未改变的variables。 他们或多或less地相同,他们积极searchUB优化部分的代码…只有普通的模式,不允许他们会打破一个太大的代码基地受到保护,我不认为你是足够普遍。

防止优化阶段合并具有相同值的variables的一个相当不好的方法就是给每一个不同的值:

 int unique_val() { static int cur = 0; // normally useless but more readable return cur++; } template <typename T> void * type_id() { static int x = unique_val(); return &x; } 

好吧,这甚至不尝试线程安全,但这不是一个问题:值永远不会被自己使用。 但是你现在有不同的variables,具有静态持续时间(按照@StoryTeller的标准,按照14.8.2的标准), 除了竞赛条件有不同的值。 由于它们使用不当,所以它们必须具有不同的地址,您应该受到保护,以便将来优化编译器的改进

注:我认为,作为价值将不会被使用,返回一个void *听起来更清洁…


只是从@bogdan评论被盗。 已知MSVC对/OPT:ICF标志具有非常积极的优化 。 讨论认为不应该是符合的,而且它只适用于标记为const的variables。 但它强制我的意见,即使OP的代码似乎符合,我不敢在生产代码没有额外的预防措施使用它。

正如@StoryTeller所提到的,它在运行时工作得很好。
这意味着你不能使用它,如下所示:

 template<int *> struct S {}; //... S<type_id<char>()> s; 

而且,这不是一个固定的标识符。 因此,您不能保证char将通过不同的可执行文件的运行绑定到相同的值。

如果你能处理这些限制,那就好了。


如果你已经知道你想要一个持久化标识符的types,你可以使用类似这样的东西(在C ++ 14中):

 template<typename T> struct wrapper { using type = T; constexpr wrapper(std::size_t N): N{N} {} const std::size_t N; }; template<typename... T> struct identifier: wrapper<T>... { template<std::size_t... I> constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {} template<typename U> constexpr std::size_t get() const { return wrapper<U>::N; } }; template<typename... T> constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}}; 

并创build您的标识符,如下所示:

 constexpr auto id = ID<int, char>; 

您可以像使用其他解决scheme那样或多或less地使用这些标识符:

 handlers[id.get<T>()] = ... 

而且,只要需要常量expression式,就可以使用它们。
作为模板参数的一个例子:

 template<std::size_t> struct S {}; // ... S<id.get<B>()> s{}; 

在switch语句中:

  switch(value) { case id.get<char>(): // .... break; case id.get<int>(): // ... break; } } 

等等。 还要注意,只要不改变ID的模板参数列表中的types的位置,它们通过不同的运行持续

主要缺点是当你引入idvariables时,你必须知道你需要一个标识符的所有types。

发表评论编辑 :我一开始没有意识到,地址被用作键,而不是int值。 这是一个聪明的方法,但它遭受了恕我直言的一个主要缺陷: 意图是非常不清楚,如果其他人发现代码。

它看起来像一个老C的黑客。 它聪明,高效,但是代码根本不能自我解释意图是什么。 在现代的C ++,IMHO,是不好的。 为程序员编写代码,而不是编译器。 除非你已经certificate有一个严重的瓶颈需要裸机优化。

我会说它应该工作,但我显然不是一个语言律师…

一个优雅但复杂的constexpr解决scheme,可以在这里或这里find

原始答案

从某种意义上说,这是“安全的”,这是有效的c ++,你可以访问所有程序中的返回指针,因为静态本地将在第一次函数调用时被初始化。 在你的代码中将会有一个Ttypes的静态variables。

但是:

  • 为什么要返回一个非const的指针? 这将允许调用者更改静态variables值,这显然不是你想要的
  • 如果返回一个const指针,我没有兴趣不返回值而不是返回指针

此外,这种获取typesid的方法只能在编译时才能使用,而不能在运行时使用多态对象。 所以它永远不会从基础引用或指针返回派生类的types。

你将如何初始化静态int值? 在这里你不要初始化它们,所以这是无效的。 也许你想使用非const指针来初始化它们的地方?

有两个更好的可能性:

1)专门为您想要支持的所有types的模板

 template <typename T> int type_id() { static const int id = typeInitCounter++; return id; } template <> int type_id<char>() { static const int id = 0; return id; //or : return 0 } template <> int type_id<unsigned int>() { static const int id = 1; return id; //or : return 1 } //etc... 

2)使用全球计数器

 std::atomic<int> typeInitCounter = 0; template <typename T> int type_id() { static const int id = typeInitCounter++; return id; } 

这最后的办法是恕我直言,因为你不必pipe理types。 正如ASH所指出的那样,基于零的递增计数器允许使用一个vector而不是一个更加简单高效的map

此外,使用unordered_map而不是map ,你不需要sorting。 这给你O(1)访问而不是O(log(n))