C ++编译时程序广泛的唯一编号

时间:2009-08-03 15:17:37

标签: c++ templates c-preprocessor

我想出了一个问题的解决方案,但我不确定它是否总是有效或只是在我的编译器上。首先,问题是:我注意到在很多情况下,有一个模板类,每次使用它时都会被重新实例化,即使给定相同的类型(比如你的模板类有静态成员初始化为函数调用)这有一些重要的副作用 - 你希望每次使用模板时都能完成这种副作用。这样做的简单方法是为模板提供一个额外的整数参数:

template<class T, class U, int uniqueify>
class foo
{
...
}

但现在你必须手动确保每次使用foo时都会为uniqueify传递一个不同的值。天真的解决方案就是像这样使用__LINE__

#define MY_MACRO_IMPL(line) foo<line>
#define MY_MACRO MY_MACRO_IMPL(__LINE__)

此解决方案存在问题 - 每个翻译单元都会重置__LINE__。因此,如果两个翻译单元在同一行上使用该模板,则该模板仅实例化一次。这看起来似乎不太可能,但想象一下,如果确实发生了调试编译错误会有多么困难。类似地,您可以尝试以某种方式使用__DATE__作为参数,但是它只有秒精度并且是编译开始的时间,而不是当它到达该行时,所以如果您使用的是并行版本,那么它是相当合理的使两个翻译单元具有相同的__DATE__

另一个解决方案是某些编译器有一个特殊的非标准宏__COUNTER__,它从0开始并在每次使用时递增。但它遇到了同样的问题 - 它会在每次调用预处理器时被重置,因此它会被重置每个翻译单元。

另一个解决方案是同时使用__FILE____LINE__

#define MY_MACRO_IMPL(file, line) foo<T, U, file, line>
#define MY_MACRO MY_MACRO_IMPL(T, U, __FILE__, __LINE__)

但是你不能根据标准将char文字作为模板参数传递,因为它们没有外部链接。

即使这确实有效,但__FILE__是否包含文件的绝对路径或文件本身的名称是否未在标准中定义,因此如果您在不同的文件夹中有两个相同的命名文件,这仍然可能会破裂。所以这是我的解决方案:

#ifndef toast_unique_id_hpp_INCLUDED
#define toast_unique_id_hpp_INCLUDED

namespace {
namespace toast {
namespace detail {

template<int i>
struct translation_unit_unique {
    static int globally_unique_var;
};

template<int i>
int translation_unit_unique<i>::globally_unique_var;

}
}
}

#define TOAST_UNIQUE_ID_IMPL(line) &toast::detail::translation_unit_unique<line>::globally_unique_var
#define TOAST_UNIQUE_ID TOAST_UNIQUE_ID_IMPL(__LINE__)

#endif

如果没有使用示例,为什么这个工作并不是很明确,但首先是概述。我的主要观点是看到每次创建全局变量或静态成员变量时,您都会以该变量的地址形式创建程序范围的唯一编号。因此,这为我们提供了一个在编译时可用的唯一编号。 __LINE__确保我们不会在同一个翻译单元中发生冲突,并且外部匿名命名空间确保变量是不同的实例(从而获得不同的地址)。

使用示例:

template<int* unique_id>
struct special_var
{
    static int value;
}

template<int* unique_id>
int special_var<unique_id>::value = someSideEffect();

#define MY_MACRO_IMPL(unique_id) special_var<unique_id>
#define MY_MACRO MY_MACRO_IMPL(TOAST_UNIQUE_ID)

foo.cpp成为:

#include <toast/unique_id.hpp>

...

typedef MY_MACRO unique_var;
typedef MY_MACRO unique_var2;
unique_var::value = 3;
unique_var2::value = 4;
std::cout << unique_var::value << unique_var2::value;

尽管是相同的模板,并且用户没有提供差异化​​参数,但unique_varunique_var2是不同的。

我最担心的是匿名命名空间中变量的地址实际上在编译时可用。从技术上讲,匿名命名空间就像声明内部链接一样,模板参数也不能具有内部链接。但标准对待匿名命名空间的方式就像变量被声明为具有程序范围唯一名称的命名空间的一部分,这意味着技术上它 具有外部链接,即使我们通常不会这样想。所以我认为标准就在我身边,但我不确定。

我不知道我是否已经做了最好的工作来解释为什么这会有用,但为了这个讨论,我发誓;)

2 个答案:

答案 0 :(得分:1)

这应该是安全的 - 但更简单的方法是使用 FILE 。此外,在64位平台上,int是不够的。使用intptr_t:

template<const char *file, int line>
class unique_value {
  static char dummy;
  unique_value() { }
public:
  static intptr_t value() { return (intptr_t)&dummy; }
};

#define UNIQUE_VALUE (unique_value<__FILE__, __LINE__>::value())

此外,请记住,在宏或模板中使用时会出现故障。

此外,具有副作用的静态值的模板是一个坏主意 - 请记住,在调用main()之前,副作用以任意顺序发生 - 并且在随机函数中隐藏初始化副作用对于可维护性不是很好。 / p>

答案 1 :(得分:1)

这项技术一般不安全,原因有两个。

  1. __LINE__在同一翻译单元中的两个不同行上可以相等,可以通过#line指令,也可以(更常见地)通过在多个头文件中使用相同的行号来实现。

  2. 如果您在头文件中的内联函数或模板定义中使用TOAST_UNIQUE_ID或从中派生的任何内容,则会出现ODR违规。

  3. 也就是说,如果你从不在头文件中使用它,并且不在主源文件中使用#line,并且每行仅使用一次宏,那么它似乎是安全的。 (您可以通过从__LINE__切换到__COUNTER__来删除最后一个限制。)