I'm learning about modern c++ and I am currently reading about template and auto type-deduction.
As a challenge I tried to write a function from a book to solve a problem, and it ended up differing very slightly to what the book suggested.
Exercise: Write a template function that returns an integral value of an enum.
My function:
template<typename T>
constexpr auto EnumToIntegral(T enumerator) noexcept
{
return std::underlying_type_t<T>(enumerator);
}
Book's function:
template<typename T>
constexpr auto EnumToIntegral(T enumerator) noexcept
{
return static_cast<std::underlying_type_t<T>>(enumerator);
}
The only difference here is the static_cast
, which I cannot figure out the point of.
Both functions compile and run as expected.
Looking at other snippets from posts regarding getting integral enum-values I also see static_cast
being used, but I've failed to find any reasoning for this.
I have tried both with, and without the static_cast
, but I get the same result.
The enum values are of type int
, and the function that is using this result is std::get
.
The fundamental casts in C++ are static_cast
, const_cast
, reinterpret_cast
and dynamic_cast
.
Each of these are able to cast expressions of certain types and value categories to certain other types. The sets of conversions permitted by each of them is different and sometimes they overlap with different semantics.
There is also the explicit type conversion (in cast notation), better known as "C-style cast" for the syntax form derived from C, in the language. With one minor exception, is however just an combination of the previously mentioned casts.
Such a cast expression will do the first sequence of casts that is possible out of the following list:
const_cast
static_cast
static_cast
followed by const_cast
reinterpret_cast
reinterpret_cast
followed by const_cast
Additionally this cast expression permits the static_cast
in this choice to ignore accessibility of base classes, i.e. it can convert a derived class pointer to a private base class pointer which no other construct in the language allows.
So, C-style casts are dangerous because they can cast almost everything to everything else with difficulty to be sure of the semantics and to avoid mistakes.
The problem is now that there are two different syntax forms for explicit type conversion which share these semantics of C-style casts:
(T)e
(cast notation, aka "C-style cast")T(e)
(functional notation)where T
is the target type and e
the source expression. The first form is generally known because it is the syntax C uses. However, the second one, which is specific to C++, still has the exact same explicit type conversion semantics of the C-style cast, but is confusing because it looks similar to
T(e1, e2)
T()
T{e}
where e1
and e2
are again expressions. While formally still explicit type conversion expressions, these do not perform a cast sequence as mentioned above but instead obtain a result via direct-initialization of an imagined variable declaration of the same form. Only if a single expression is used in parentheses, not braces, it is an explicit type conversion with the powers mentioned above.
So, if one wants to be consistent about avoiding C-style casts, one should also avoid expressions of the form T(e)
.
But that is what you are using. static_cast
is more explicit, stating what fundamental cast you want, not that the compiler should attempt the list mentioned above and potentially make a wrong choice.
In this particular example there is no difference, because an expression with enumeration type can always be cast to its underlying type with a static_cast
, so that the second entry in the list is guaranteed to be chosen. But that may not be obvious.
Actually, the form T{e}
(direct-list-initialization) is, typically, even weaker than static_cast
, while still stronger than an implicit conversion. But it wouldn't suffice here because it doesn't allow the conversion for scoped enumeration types.
(Except for initialization with braces, none of this has anything to do with modern C++ features btw. These rules have been present since the first C++ standard C++98.)