使用函数模板中静态局部变量的地址作为类型标识符是否安全



我希望创建一个不需要RTTI:的std::type_index的替代方案

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

注意,局部变量x的地址被用作类型ID,而不是x本身的值。此外,我不打算在现实中使用裸指针。我刚刚把所有与我的问题无关的东西都删掉了。请在此处查看我的实际type_index实现。

这种方法合理吗?如果合理,为什么?如果没有,为什么不呢?我觉得我在这里的基础不稳固,所以我对我的方法是否有效的确切原因感兴趣。

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

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的回答,因为他概述了为什么根据C++规则,解决方案应该是有效的。然而,@SergeBallesta和其他一些人在评论中指出,MSVC执行的优化令人不安地接近于打破这种方法。如果需要更稳健的方法,那么使用std::atomic的解决方案可能更可取,如@galinette:所建议的那样

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>()的同一静态局部变量的地址。您在这里受到标准的保护,不受ODR违规行为的影响。

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

这与标准一致,因为C++使用模板,而不是像Java那样具有类型擦除的泛型,所以每个声明的类型都有自己的函数实现,其中包含一个静态变量。所有这些变量都是不同的,因此应该有不同的地址。

问题是他们的从未被使用过,更糟糕的是从未改变过。我记得优化器可以合并字符串常量。由于优化器尽其所能比任何人类程序员都聪明得多,我担心过于热心的优化编译器会发现,由于这些变量值从未更改,它们都会保持0值,所以为什么不将它们全部合并以节省内存呢?

我知道,由于"好像"规则,编译器可以自由地做它想做的事情,前提是可观察的结果是相同的。我不确定总是共享相同值的静态变量的地址是否不同。也许有人可以确认标准的哪一部分真正关心它?

当前的编译器仍然单独编译程序单元,因此无法确定另一个程序单元是否会使用或更改该值。因此,我认为优化器将没有足够的信息来决定合并变量,并且您的模式是安全的。

但由于我真的不认为这个标准可以保护它,我不能说未来版本的C++构建器(编译器+链接器)是否不会发明一个全局优化阶段,主动搜索可以合并的未更改变量。或多或少与他们主动搜索UB以优化部分代码相同。。。只有常见的模式,不允许它们会破坏太大的代码库,才受到保护,我认为你的模式还不够常见。

防止优化阶段合并具有相同值的变量的一种相当巧妙的方法是给每个变量一个不同的值:

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;
}

好吧,这甚至不试图是线程安全的,但这不是问题:这些值本身永远不会被使用。但现在有不同的变量具有静态持续时间(根据@StoryTeller所说的标准14.8.2),除了在比赛条件下具有不同的值。当它们被odr使用时,它们必须有不同的地址,并且您应该受到保护,以便在未来优化编译器的改进。。。

注意:我认为由于不会使用该值,所以返回void *听起来更干净。。。


只是从@bogdan的评论中偷来的一个。已知MSVC利用/OPT:ICF标志具有非常积极的优化。讨论表明is不应该是一致的,并且它只适用于标记为const的变量。但它强化了我的观点,即即使OP的代码看起来是一致的,如果在生产代码中没有额外的预防措施,我也不敢使用它。

正如@StoryTeller所提到的,它在运行时运行良好
这意味着你不能按如下方式使用它:

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

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

如果你能克服这些限制,那也没关系。


如果你已经知道你想要一个持久标识符的类型,你可以使用这样的东西(在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)>{}};

并按如下方式创建标识符:

constexpr auto id = ID<int, char>;

您可以或多或少地使用这些标识符,就像您使用其他解决方案一样:

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

此外,只要需要常量表达式,就可以使用它们
作为模板参数的示例:

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的模板参数列表中的位置,它们在不同的运行中都是持久的。

主要缺点是,在引入id变量时,必须知道需要标识符的所有类型。

后注释编辑:我一开始没有意识到地址被用作键,而不是int值。这是一个聪明的方法,但它给IMHO带来了一个主要缺陷:如果其他人发现了该代码,的意图就非常不清楚。

它看起来像一个旧的C破解。它聪明、高效,但代码根本无法自我解释其意图。在现代c++中,imho是糟糕的。为程序员而不是编译器编写代码。除非您已经证明存在严重的瓶颈,需要进行裸机优化。

我会说这应该有效,但我显然不是一名语言律师。。。

一个优雅但复杂的constexpr解决方案,可以在这里或这里找到

原始答案

它是"安全的",因为它是有效的c++,并且您可以访问所有程序中返回的指针,因为静态本地将在第一次函数调用时初始化。在代码中使用的每种类型T都将有一个静态变量。

但是:

  • 为什么返回非常量指针?这将允许调用方更改静态变量值,这显然不是您想要的
  • 如果返回const指针,我认为不按值返回而不是返回指针是没有兴趣的

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

如何初始化静态int值?这里你没有初始化它们,所以这是无效的。也许您想在某个地方使用非常量指针来初始化它们?

有两种更好的可能性:

1)为您想要支持的所有类型专门化模板

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;
}

最后一种方法是IMHO更好的方法,因为您不必管理类型。正如A.S.H所指出的,基于零的递增计数器允许使用vector而不是map,这更简单有效。

此外,使用unordered_map而不是map,您不需要订购。这为您提供O(1)访问权限,而不是O(log(n))

最新更新