In a type trait, why do people use enum rather tha

2019-03-14 14:39发布

问题:

For example, this is how I would write it, and it compiles and works just fine:

template<typename T> struct is_pointer<T*> {
  static const bool value = true;
}

Then why do some people write the less obvious

template<typename T> struct is_pointer<T*> {
  enum { value = true };
}      

instead? Is it only because the static const variable uses a byte of memory, whereas the enum doesn't?

回答1:

A notable difference is in the fact that the following code compiles and links:

template<typename>
struct is_pointer { };

template<typename T>  
struct is_pointer<T*> {
  enum { value = true };
};     

void f(const bool &b) { }

int main() {
  f(is_pointer<void*>::value);
}

The following does not work instead (you get an undefined reference to value):

template<typename>
struct is_pointer { };

template<typename T>
struct is_pointer<T*> {
  static const bool value = true;
};

void f(const bool &b) { }

int main() {
  f(is_pointer<void*>::value);
}

Of course, it doesn't work unless you add somewhere the following lines:

template<typename T>
const bool is_pointer<T*>::value;

That is because of [class.static.data]/3 (emphasis mine):

If a non-volatile non-inline const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression ([expr.const]). The member shall still be defined in a namespace scope if it is odr-used ([basic.def.odr]) in the program and the namespace scope definition shall not contain an initializer. [...]

In other terms, static const bool value = true; is a declaration, not a definition and you cannot odr-use value.
On the other side, according with [dcl.enum/1] (emphasis mine):

An enumeration is a distinct type with named constants.

Those named constants can be const referenced as shown in the example above.


As a side note, something similar applies if you use static constexpr data members in C++11/14:

template<typename T>
struct is_pointer<T*> { static constexpr bool value = true; }; 

This doesn't work as well and that's how I discovered the subtle differences between them.

I found help here on SO getting some nice hints out of the answer I've been given.
References to the standard are a plus to better explain what's going on under the hood.

Note that a static constexpr data member declaration like the one above is also a definition since C++17. Therefore you won't have to define it anymore and you'll be able to odr-use it directly instead.


As mentioned in the comments (thanks to @Yakk that confirmed this) I'm also trying to explain how it happens that the above mentioned named constants bind to a const reference.

[expr.const/3] introduces the integral constant expression and mentions unscoped enums by saying that it's implicitly converted to a prvalue.
[dcl.init.ref/5] and [class.temporary/2] do the rest, for they rule on reference binding and temporaries.



回答2:

Is it only because the static const variable uses a byte of memory, whereas the enum doesn't?

Yes, that's the reason.

static const bool value = true;

would occupy memory, while

enum { value = true };

doesn't.



回答3:

Yes you are correct: enum { value = true }; doesn't occupy any memory.

Furthermore, prior to C++11 it was pretty much the only way of achieving this: static const bool value = true; is only legal in a class definition from C++11 onwards. Although a constexpr might be preferred.



回答4:

It also is another symbol in every object file that includes it, to no benefit. If you use symbol folding (--gc-sections) you'll run out of separatable sections & bloat your binary.



回答5:

Some people write the less obvious enum rather than static bool const because they don't realize that there are other changes they should make.

C++ requires the object to be defined if it's address is needed, for example if it's passed to this function foo:

void foo(bool const &);

However, solving the issue by defining the object is actually not the correct fix for this problem. Here are some alternatives:

  1. Small objects should not be passed by reference. The change should be to remove const & from the function signature, not add a definition for the object.

  2. Where the function signature cannot be changed, a temporary can be created explicitly in the call: foo( bool { Cls::mbr } )

  3. However, this is compile time information! Therefore foo should be a template with a T and T* overload, or be specialized with bool.

This 3rd solution has the benefit of removing an unnecessary run time check (hopefully optimized by the compiler) and also allowing for the pointer and non-pointer case to be handled independently, possibly making the code clearer.