当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《在函数模板中使用静态局部变量的地址作为类型标识符是否安全?》经验,为你挑选了4个好方法。

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

template 
int* type_id() {
    static int x;
    return &x;
}

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

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

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

class processor {
public:
    template 
    void register_handler(Handler handler) {
        handlers[type_id()] = [handler](void const* v) {
            handler(*static_cast(v));
        };
    }

    template 
    void process(T const& t) {
        auto it = handlers.find(type_id());
        if (it != handlers.end()) {
            it->second(&t);
        } else {
            throw std::runtime_error("handler not registered");
        }
    }

private:
    std::map> handlers;
};

这个类可能会这样使用:

processor p;

p.register_handler([](int const& i) {
    std::cout << "int: " << i << "\n";
});
p.register_handler([](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 
std::size_t type_id() {
    static std::size_t const x = type_id_counter++;
    return x;
}

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



1> StoryTeller ..:

是的,它在某种程度上是正确的.模板函数是隐式的inline,函数中的静态对象在inline所有翻译单元之间共享.

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

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


@SergeBallesta - 事实上[§14.82](http://eel.is/c++draft/temp#fct.spec-2)
@SergeBallesta如果他们的地址是可观察的({\ [intro.object \]/8](http://eel.is/c++draft/intro.object#8)).也就是说,我已经看到MSVC与[`/opt:icf`](https://msdn.microsoft.com/en-us/library/bxwfs976.aspx)链接器选项以非标准方式合并一些COMDAT(它至少记录的行为).我不确定OP的解决方案是否会受到影响,但我给出的是[这里](/sf/ask/17360801/ -static-local-variable-a-function-templ#comment70922303_41868897)不应该,因为变量`id`不是`const`.

2> Serge Balles..:

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

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

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

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

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

防止优化阶段合并具有相同值的变量的相当愚蠢的方法只是为每个变量赋予不同的值:

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

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

注意:我认为由于不会使用该值,因此返回void *声音更清晰......


只是从@bogdan的评论中偷走了一个额外的东西.众所周知,MSVC对/OPT:ICF标志进行了非常积极的优化.讨论表明它不应该符合,并且它只适用于标记为const的变量.但它强制执行我的观点,即使OP的代码看起来符合要求,如果没有生产代码中的额外预防措施,我也不敢使用它.


"而且我不确定永远共享相同值的静态变量的地址是否应该不同." - "如果一个是另一个的子对象,或者如果至少有一个是零大小的基类子对象并且它们的类型不同,那么非位域的两个对象可能具有相同的地址;否则,它们应具有不同的地址".(N4140中的§1.8/ 6).没有必要浪费时间来防范潜在的未来疯狂IMO.
如果声明静态不稳定怎么办?这会阻止优化器吗?
`uintptr_t x =(uintptr_t)&x;`合并那个,你聪明的裤子编译器!

3> galinette..:

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

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

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

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

原始答案

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

但是:

为什么返回非常量指针?这将允许调用者更改静态变量值,这显然不是您想要的

如果返回一个const指针,我认为没有兴趣不返回值而不是返回指针

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

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

有两种更好的可能性:

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

template 
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

template <>
int type_id() {
    static const int id = 0;
    return id;  //or : return 0
}

template <>
int type_id() {
    static const int id = 1;
    return id;  //or : return 1
}

//etc...

2)使用全局计数器

std::atomic typeInitCounter = 0;

template 
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

最后一种方法是恕我直言,因为你不必管理类型.正如ASH所指出的,基于零的递增计数器允许使用a vector而不是a map更简单和有效.

另外,使用an unordered_map而不是a map,您不需要订购.这给你O(1)访问而不是O(log(n))


它们是否会改变静态值并不重要.重要的是它的地址.你有点错过了这一点.然而,关于它只是一个运行时的事情的单句是正确和最大的警告.

4> skypjack..:

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

template
struct S {};

//...

S()> s;

而且,它不是固定的标识符.因此,您无法保证char通过可执行文件的不同运行绑定到相同的值.

如果你可以处理这些限制,那就没关系了.


如果您已经知道要使用持久标识符的类型,则可以使用类似的内容(在C++ 14中):

template
struct wrapper {
    using type = T;
    constexpr wrapper(std::size_t N): N{N} {}
    const std::size_t N;
};

template
struct identifier: wrapper... {
    template
    constexpr identifier(std::index_sequence): wrapper{I}... {}

    template
    constexpr std::size_t get() const { return wrapper::N; }
};

template
constexpr identifier ID = identifier{std::make_index_sequence{}};

并按照以下方式创建您的标识符:

constexpr auto id = ID;

您可以像使用其他解决方案一样使用这些标识符:

handlers[id.get()] = ...

此外,您可以在需要常量表达式的任何地方使用它们.
作为模板参数的示例:

template
struct S {};

// ...

S()> s{};

在switch语句中:

    switch(value) {
    case id.get():
         // ....
         break;
    case id.get():
        // ...
        break;
    }
}

等等.另请注意,只要不更改模板参数列表中类型的位置,它们就会通过不同的运行持久化ID.

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

推荐阅读
乐韵答题
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有