Enumeration values versus static constants in metaprogramming

Viewed 80

I'm reading C++ Templates: The Complete Guide, chapter 23. Metaprogramming. At the end, it describes the difference in using enumeration values versus static constants in metaprogramming. Consider the following two implementations of calculating Nth power of 3:

Enumeration implementation:

// primary template to compute 3 to the Nth
template<int N>
struct Pow3 {
   enum { value = 3 * Pow3<N-1>::value };
};

// full specialization to end the recursion
template<>
struct Pow3<0> {
   enum { value = 1 };
};

Static constant implementation:

// primary template to compute 3 to the Nth
template<int N>
struct Pow3 {
   static int const value = 3 * Pow3<N-1>::value;
};
// full specialization to end the recursion
template<>
struct Pow3<0> {
   static int const value = 1;
};

Right after that it says there is a drawback to the latter version. If we got a function void foo(int const&); and pass it the result of metaprogram: foo(Pow3<7>::value); the book says:

A compiler must pass the address of Pow3<7>::value, and that forces the compiler to instantiate and allocate the definition for the static member. As a result, the computation is no longer limited to a pure “compile-time” effect. Enumeration values aren’t lvalues (i.e., they don’t have an address). So, when we pass them by reference, no static memory is used. It’s almost exactly as if you passed the computed value as a literal.

I don't understand their explanation at all to be honest. I thought in the static constant version we evaluate the result at compile time, but the actual referencing happen in run-time because we need to pass the adress. But then I don't see much difference in the former case (enum implementation) because we should have temporary materialization there (prvalue -> xvalue conversion) and since temporary objects also have the adress my thinking process fails. Also it says "no static memory is used" which I don't understand as well since static memory should refer to allocation that happen at compile-time.

Could someone explain this whole thing in greater detail?

2 Answers

I suggest looking at it this way:
There is a difference between a type, an instance of a type, and the values said type can take.

In the first case you are specifying a type (an unnamed enum) with only one possible value value (which is the compile-time constant).
There are no instantiations of the type, so no memory will be used compile-time or runtime.
Every time the code refers to Pow3<N>::value, the compiler will not create an instance of the type but will instead use the constant directly.

In the second case you are instead specifying a variable value of type int, and assigning the compile-time constant to it.
This variable exists, so memory will be used.
Every time the code refers to Pow3<N>::value, the compiler will use said variable.

(enum implementation) because we should have temporary materialization there (prvalue -> xvalue conversion) and since temporary objects also have the address

they do, but the temporary xvalue only exists for the duration of the function call. It's identical to passing the function an integer literal. So, an addressable object exists temporarily, at runtime, with automatic scope.

Conversely,

... that forces the compiler to instantiate and allocate the definition for the static member. As a result, the computation is no longer limited to a pure “compile-time” effect.

This static const int is an object with static storage duration. There's no temporary materialization necessary, it's an object, it existed when your program started, and you're taking a reference to it.

If you write multiple .cpp files including the header with Pow3, and they all call foo(Pow3<3>):

  • the enum version will emit something like foo(27) in each translation unit, with three unrelated temporary xvalues
  • the static version will emit a global object equivalent to const int Pow3<3>::value = 27; and each translation unit will refer to (ie, take a reference to) the same object
Related