Unique, Up To a Point

The other day I had a need to generate a unique type for each time a given constexpr function was called (why I needed this is a long story and the answer will have to wait for the library I’m working on to be released as open source). I wanted a way to do this that would be the least error prone possible (i.e. as much as possible I didn’t want the caller of the function to have to pass something that they could get wrong ruining the uniqueness). I came up with a solution, but it requires macros (I know, gross). Specifically, I used the __COUNTER__ macro, which, though it isn’t standard, is supported by the big three compilers (and really I only needed to worry about Microsoft’s compiler for my purposes, but maximizing the cross-platform compatibility never hurts).

So to start with, we need to be able to generate a new type on demand at compile time. Something like

constexpr UniqueType GenerateUniqueType()

where UniqueType will be a new type for every call to GenerateUniqueType. We could go whole hog with the macros and just generate class names using token pasting. But it’s bad enough that we’re using macros at all, lets at least limit their use as much as possible. So instead we’ll use a template class with an integral parameter:

template<size_t N>
struct UniqueType

For each value of N we’ll get a different type, the problem is generating the values for N. When it comes to constexpr there is no mutable global state. With C++14 you can have mutable state within a constexpr function, but no function static data, so that state can’t persist from one call to another. If we’re willing to stoop to macros (and a non-standard macro at that) we can get a very constrained type of global mutable state using the __COUNTER__ macro. This macro expands to zero the first time you use it and then increments the result each time it gets expanded after that. So we could do something like

constexpr auto GenerateUniqueType()
   return UniqueType<__COUNTER__>();

Unfortunately, we’ll end up generating all the same type in this case as the macro is only expanded once when the preprocessor hits our function definition. Instead we need to expand __COUNTER__ every time we call GenerateUniqueType, like so

template<size_t N>
constexpr auto GenerateUniqueType()
   return UniqueType<N>();

constexpr auto unique = GenerateUniqueType<__COUNTER__>();

Making the callers of GenerateUniqueType provide a __COUNTER__ expansion each time they call the function is just asking for trouble so we should wrap it in another macro to make the __COUNTER__ expansion automatic

template<size_t N>
constexpr auto GenerateUniqueType()
   return UniqueType<N>();

#define GEN_UNIQUE_TYPE GenerateUniqueType<__COUNTER__>

constexpr auto unique = GEN_UNIQUE_TYPE();

So, yeah, it’s a bit gross with all the macros (with a double macro expansion at that). But it does seem like a somewhat restrained use of macros, all things considered. And it actually gets worse: in the code where I am using this technique the macros are also variadic as I need to pass an unspecified number of arguments to the function that the macro is wrapping.

While this does accomplish what we set out to do, generate a unique type for each call to GenerateUniqueType, there are some caveats as things can be both not unique enough and too unique depending on how and where we do things.

First off, __COUNTER__ only provides unique values within a single compilation unit. If we call GenerateUniqueType in two different compilation units we could end up with multiple objects of type UniqueType<N> with the same N. Given that there is the chance that every compilation unit could be compiled with a different instance of the compiler this non-uniqueness between compilation units shouldn’t be too surprising. In order for different compilation units to ensure unique expansions of __COUNTER__ we would need to always have a single instance of the compiler or all the instances would need to coordinate their __COUNTER__ values. That seems like a lot to ask, especially in the age of distributed builds. For what I’m working on I only really need the types to be unique within any given compilation unit so I can live with the non-uniqueness (the stuff I had to go back and rework once I realized this problem existed not withstanding).

Now imagine you make the mistake of putting a call to GenerateUniqueType in a header file. You probably meant for the object that you get to have the same type in every compilation unit that the header is included in. Unless you are really lucky this won’t be the case: the GEN_UNIQUE_TYPE macro will get expanded separately in each translation unit with the __COUNTER__ macro most likely expanding to a different value each time. And even if it does expand to the same value each time it probably won’t the next time you compile if you aren’t extremely careful about what changes you make in each compilation unit. That’s things being "too unique". Unfortunately this constrains objects that are created using GEN_UNIQUE_TYPE to only be used in a single compilation unit as there’s no way to export the type through a header. As with things not being unique enough, this isn’t a big deal for my purposes: the objects I’m creating aren’t needed outside the compilation unit they’re created in. If they were, the best you could do would be to derive the unique type from a non-parameterized base class that you could export through the header and provide functions that returned base-class references to the objects (of course you can still only use the objects as constexpr in the compilation unit, the exported references can only be used at run-time).

So, GEN_UNIQUE_TYPE comes with two restrictions: don’t rely on types generated in two different compilation units to be unique and don’t use it in a header.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: