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

Is it safe to use the address of a static local variable within a function template as a type identifier?

我想创建 std::type_index that does not require RTTI 的替代方案:

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

注意,局部变量x的地址用作类型ID,而不是x本身的值。另外,我不打算在现实中使用裸指针。我刚刚删除了与我的问题无关的所有内容。查看我的实际 type_index 实现 here

这种方法合理吗?如果是,为什么?如果不是,为什么不呢?我觉得我在这里站不住脚,所以我对我的方法有效或无效的确切原因很感兴趣。

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

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

这个class可以这样使用:

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

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

Post-comment edit : 我一开始没意识到地址被用作键,而不是 int 值。这是一个聪明的方法,但恕我直言,它有一个主要缺陷:如果其他人找到该代码,intent 非常不清楚。

它看起来像一个旧的 C hack。它很聪明、高效,但代码根本没有 self-explain 的意图。在现代 C++ 中,恕我直言,这很糟糕。为程序员而不是编译器编写代码。除非你已经证明存在严重的瓶颈需要裸机优化。

我会说它应该有用,但我显然不是语言律师...

可以找到一个优雅但复杂的 constexpr 解决方案 here or

原回答

它是 "safe",因为这是有效的 c++,您可以在所有程序中访问 returned 指针,因为静态局部变量将在第一次函数调用时初始化。您的代码中使用的每种类型 T 将有一个静态变量。

但是:

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

此外,这种获取类型 ID 的方法仅在编译时有效,在 运行 多态对象时无效。所以它永远不会 return 来自基引用或指针的派生 class 类型。

您将如何初始化静态 int 值?在这里你没有初始化它们所以这是无效的。也许您想使用非 const 指针在某处初始化它们?

有两种更好的可能性:

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

恕我直言,最后一种方法更好,因为您不必管理类型。正如 A.S.H 所指出的,zero-based 递增计数器允许使用 vector 而不是 map,后者更加简单高效。

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

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

因此,在每个翻译单元中,您将获得与调用 type_id<Type>() 相同的静态局部变量的地址。根据标准,您在这里不受 ODR 违规的影响。

因此,本地静态的地址可以用作一种home-brewed run-time类型标识符。

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

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

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

目前的编译器仍然是单独编译程序单元,所以他们无法确定另一个程序单元是否会使用或更改该值。所以我的意见是优化器没有足够的信息来决定合并变量,你的模式是安全的。

但我真的不认为标准会保护它,我不能说未来版本的 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),except in race conditions 有不同的值。由于它们是 ODR 使用的,因此它们必须具有不同的地址,您应该受到保护,以便将来 改进 优化编译器...

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


@bogdan 的评论 stolen 的补充。 。讨论表明 is 不应该是一致的,它只适用于标记为 const 的变量。但它强化了我的观点,即即使 OP 的代码看起来是一致的,如果在生产代码中没有额外的预防措施,我也不敢使用它。

所述,它在运行时.
工作正常 这意味着你不能使用它如下:

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.

的模板参数列表中的位置,它们就会通过不同的运行 persistent

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