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

时间:2017-01-26 06:41:50

标签: c++ templates language-lawyer rtti

我希望创建std::type_index的替代方案,不需要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;
};

这个类可能会这样使用:

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

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

4 个答案:

答案 0 :(得分:27)

是的,在某种程度上是正确的。模板函数隐式@media (max-width: 576px) { nav .container { width: 100%; } } inline函数中的静态对象在所有翻译单元中共享。

因此,在每个翻译单元中,您将获得调用inline的相同静态局部变量的地址。此处受到标准的ODR违规保护。

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

答案 1 :(得分:11)

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

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

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

当前编译器仍然单独编译程序单元,因此无法确定其他程序单元是否将使用或更改该值。所以我的观点是优化器没有足够的信息来决定合并变量,你的模式是安全的。

但由于我真的不认为该标准可以保护它,我不能说未来版本的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标准),除了竞争条件具有不同的值。由于它们使用得太多,它们必须具有不同的地址,您应该受到保护,以便将来优化编译器改进 ...

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

来自@bogdan的评论只是添加被盗MSVC is known to have very aggressive optimization with the /OPT:ICF flag。讨论表明它不应该符合,并且它只适用于标记为const的变量。但它强制执行我的观点,即使OP的代码看起来符合要求,如果没有生产代码中的额外预防措施,我也不敢使用它。

答案 2 :(得分:6)

评论后编辑:我刚开始没有意识到该地址被用作键,而不是int值。这是一个聪明的方法,但它遭受了恕我直言的一个重大缺陷:意图非常不清楚是否有其他人找到该代码。

它看起来像一个老C黑客。它的聪明,高效,但代码并不能自我解释意图是什么。在现代c ++中,imho,这很糟糕。为程序员编写代码,而不是编译器。除非你已经证明存在严重的瓶颈,需要进行裸机优化。

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

可以找到优雅但复杂的constexpr解决方案herehere

原始回答

安全&#34;从某种意义上说这是有效的c ++,您可以在所有程序中访问返回的指针,因为静态local将在第一次函数调用时初始化。每个类型T都会有一个静态变量用在你的代码中。

但是:

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

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

您将如何初始化静态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所指出的那样,从零开始的增量计数器允许使用vector而不是map,这更简单有效。

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

答案 3 :(得分:6)

正如@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变量时,您必须知道需要标识符的所有类型。