Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ compile time program wide unique numbers

I've come up with a solution to a problem but I'm not sure if it'll always work or just on my compiler. First, the problem: I've noticed in a number of situations it's desirable to have a template class that gets re-instantiated each time it's used even when given the same types (say your template class has static members that are initialized to function calls that have some important side effect -- and you want this side effect to be done every time the template is used). The easy way to do this is to give your template an extra integer parameter:

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

But now you have to manually make sure that every time you use foo you pass it a different value for uniqueify. The naive solution is to use __LINE__ like this:

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

This solution has an issue though -- __LINE__ gets reset for each translation unit. So if two translation units use the template on the same line, the template only gets instantiated once. That may seem unlikely, but imagine how difficult to debug the compiler error it would be if it did happen. Similarly you could try using __DATE__ as a parameter somehow, but that only has seconds precision and it's the time when compiling started, not when it reaches that line, so if you're using a parallel version of make it's rather plausible to have two translation units with the same __DATE__.

Another solution is that some compilers have a special non-standard macro, __COUNTER__ that starts at 0 and increments every time you use it. But it suffers from the same problem -- it gets reset for each invocation of the preprocessor, so it gets reset each translation unit.

Yet another solution, is to use __FILE__ and __LINE__ together:

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

But you can't pass char literals as template parameters according to the standard because they don't have external linkage.

Even if this did work, whether __FILE__ contains the absolute path to the file or just the name of the file itself isn't defined in the standard, so if you had two identical named files in different folders, this could still break. So here is my solution:

#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

Why this works isn't really clear without a usage example, but first an overview. The key insight I had was to see that every time you make a global variable or a static member variable, you're creating a program wide unique number in the form of the address of that variable. So this gives us a unique number that's available at compile time. __LINE__ makes sure we won't get clashes within the same translation unit, and the outer anonymous namespace makes sure the variables are different instances (and thus get differing addresses) across translation units.

Example usage:

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)

And foo.cpp becomes:

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

Despite being the same template, and the user providing no differentiating parameters, unique_var and unique_var2 are distinct.

I'm mostly worried about the address in of the variable in the anonymous namespace actually being available at compile time. Technically, an anonymous namespace is like declaring internal linkage, and template parameters can't have internal linkage. But the way the standard says to treat anonymous namespaces is just like the variable was declared as part of a namespace with a program-wide unique name, which means that technically it does have external linkage, even though we don't usually think of it as such. So I think the standard is on my side, but I'm not sure.

I don't know if I've done the best job of explaining why this would be useful, but for the sake of this discussion, it is, I swear ;)

like image 879
Joseph Garvin Avatar asked Oct 26 '22 03:10

Joseph Garvin


1 Answers

This technique is not safe in general, for two reasons.

  1. __LINE__ can be equal on two different lines in the same translation unit, either through #line directives, or (more commonly) through use on the same line number in multiple header files.

  2. You will have ODR violations if you use TOAST_UNIQUE_ID or anything derived from it within in inline function or template definition in a header file.

That said, if you never use this in header files, and don't use #line in your main source file, and only use the macro once per line, it seems safe. (You can remove that last restriction by switching from __LINE__ to __COUNTER__.)

like image 141
Richard Smith Avatar answered Nov 12 '22 22:11

Richard Smith