Is there any guarantee on the order of substitution in a function template after type deduction? - c++

Consider this function template:
template<typename T>
typename soft_error<T>::type foo(T, typename hard_error<T>::type)
{ }
After deducing type T from the type of the first argument in the call to foo(), the compiler will proceed to substitute T and instantiate the function signature.
If substitution for the return type gets executed first, causing a simple substitution failure, the compiler will discard this function template when computing the overload set and search for other viable overloads (SFINAE).
On the other hand, if substitution for the second function parameter occurs first, causing a hard error (e.g. because of a substitution failure in a non-immediate context), the entire compilation would fail.
QUESTION: Is there any guarantee on the order in which substitution will be performed for the function parameters and return types?
NOTE: This example seems to show that on all major compilers (VC11 was tested separately and gave identical results) substitution for the return type occurs before substitution for the parameter types.

[NOTE: This was not originally meant to be a self-answered question, but I happened to find out the solution while crafting the question]
Is there any guarantee on the order in which substitution will be performed for the function parameters and return types?
Not in the current standard.
However, this Defect Report (courtesy of Xeo) shows that this is indeed intended to be the case. Here is the proposed new wording for Paragraph 14.8.2/7 of the C++11 Standard (which has become part of the n3485 draft):
The substitution occurs in all types and expressions that are used in the function type and in template
parameter declarations. The expressions include not only constant expressions such as those that appear in
array bounds or as nontype template arguments but also general expressions (i.e., non-constant expressions)
inside sizeof, decltype, and other contexts that allow non-constant expressions. The substitution proceeds
in lexical order and stops when a condition that causes deduction to fail is encountered. [...]
As correctly pointed out by Nicol Bolas in the comments to the question, lexical order means that a trailing return type would be substituted after the parameter types, as shown in this live example.

Related

What normative rules govern and allow SFINAE?

SFINAE, "Substitutation Failure Is Not An Error", is a well-known rule/technique applied during overload resolution of function templates (see e.g. SFINAE # cppreference). However, it is never referred to by that name in the C++ standard; the only reference I can find is that of an implementation-dependent macro from <version>, as specified in [version.syn]/2
#define __cpp_­lib_­result_­of_­sfinae 201210L // also in <functional>, <type_­traits>
What is(/are) the normative reference(s) for this rule/technique?
Let's start from an unqualified function call (f(1) below), particularly in an example constructed such that the postfix-expression of the function call names two function templates, one of which we expect to be rejected by SFINAE.
#include <type_traits>
// A: expected to be viable
template<typename T, std::enable_if_t<std::is_integral_v<T>>* = nullptr>
constexpr bool f(T) { return true; }
// B: expected to be rejected
template<typename T, std::enable_if_t<!std::is_integral_v<T>>* = nullptr>
constexpr bool f(T) { return false; }
static_assert(f(1));
The function call above will, as per [over.match.call.general]/1, result in overload resolution applied as specified in [over.call.func], particularly [over.call.func]/3:
[over.match.call.general]/1 In a function call
postfix-expression ( expression-list_opt )
if the postfix-expression names at least one function or function template, overload resolution is applied as specified in [over.call.func].
[over.call.func]/3 [...] The name is looked up in the context of the function call following the normal rules for name lookup in expressions ([basic.lookup]). The function declarations found by that lookup constitute the set of candidate functions. [...]
[over.match.general]/2 decribes the high-level steps of overload resolution:
Overload resolution selects the function to call in seven distinct contexts within the language:
(2.1) invocation of a function named in the function call syntax;
[...]
Each of these contexts defines the set of candidate functions and the list of arguments in its own unique way. But, once the candidate functions and argument lists have been identified, the selection of the best function is the same in all cases:
[...]
Telling us that selecting a function during overload resolution works in three different phases:
Identify the candidate functions and associated argument lists
From these, select the subset of viable functions
From these, select the best viable function based on ranking via implicit conversion sequences matching arguments with parameters
SFINAE works in the domain of 1 whilst identifying candidate functions, not so much by rejecting potential candidates but rather by failing to generate a potential candidate when such a potential candidate is a function template (specialization), particularly failing due to template deduction failures. Our example is in the context of invocation of a function named in the function call syntax, but for the context of finding the normative rules that governs SFINAE, it is representable also for other contexts as listed by [over.match.general]/2.
[over.match.funcs], particularly [over.match.funcs.general]/7 tells us that we enter the domain of template argument deduction for candidates that are function templates:
In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction ([temp.over], [temp.deduct]) [...] Those candidates are then handled as candidate functions in the usual way.
[temp.over] covers overload resolution rules specific to function templates and [temp.over]/1, in particular, contains one of two essential rules which govern SFINAE:
When a call to the name of a function or function template is written [...] template argument deduction [...] [is] performed [...] if the argument deduction and checking succeeds [...] [the] function template specialization [...] is added to the candidate functions set to be used in overload resolution. If, for a given function template, argument deduction fails or the synthesized function template specialization would be ill-formed, no such function is added to the set of candidate functions for that template.
Namely that a template argument deduction failure in the phase of finding candidate functions during overload resolution results in the function not being included in the set of candidates.
[temp.deduct] covers template argument deduction, and includes several ways in which it can fail, e.g. [temp.deduct]/2, /5, and /7, but the second essential rule which governs SFINAE is particularly found in [temp.deduct]/8:
/7 The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. [...] The substitution proceeds in lexical order and stops when a condition that causes deduction to fail is encountered. [...]
/8 If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed, with a diagnostic required, if written using the substituted arguments.
[...]
Only invalid types and expressions in the immediate context of the function type, its template parameter types, and its explicit-specifier can result in a deduction failure.
The latter paragraph of [temp.deduct]/8 is the reason for some notoriously tricky non-SFINAE hard failures when using generic lambdas in return type deduction. See e.g. P0238R1 for details.
If we return to the function call f(1) in the example above, the function template B will be rejected as a candidate as template argument deduction fails when replacing T in std::enable_if_t<!std::is_integral_v<T>>*, namely in the 2nd type template parameter of the template-head, with substitution of the deduced T (int) resulting in an invalid type (std::enable_if_t<!std::is_integral_v<int>>*).
[temp.deduct]/5 When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template and the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails.
Finally, [temp.deduct]/5 is also where we find rules for constraints, which are not a SFINAE mechanism, but a separate step where non-satisfaction results in type deduction failure.
/5 [...] If the function template has associated constraints ([temp.constr.decl]), those constraints are checked for satisfaction ([temp.constr.constr]). If the constraints are not satisfied, type deduction fails.

Dependent non-type parameter packs: what does the standard say?

I think the following code is well-formed:
template< typename T >
using IsSigned = std::enable_if_t< std::is_signed_v< T > >;
template< typename T, IsSigned< T >... >
T myAbs( T val );
Others say that it is ill-formed, because §17.7 (8.3) of the C++17 standard:
Knowing which names are type names allows the syntax of every template to be checked. The program is ill-formed, no diagnostic required, if: (...) every valid specialization of a variadic template requires an empty template parameter pack, or (...)
In my opinion IsSigned< T >... is a dependent template parameter, therefore it can not be checked against §17.7 (8.3) in template definition time. IsSigned< T > could be for example void for one subset of Ts, int for another subset or substitution failure. For the void subset it is true, that the empty template parameter pack would be the only valid specialization, but the int subset could have many valid specializations. It depends on the actual T argument.
It means that the compiler must check it after the template instantiation, because T is not known before. At that point the full argument list is known, there is zero variadic arguments. The standard says the following (§17.6.3 (7)):
When N is zero, the instantiation of the expansion produces an empty list. Such an instantiation does not alter the syntactic interpretation of the enclosing construct
This is why I think it is well formed.
What do you think?
How can I track down this ambiguity for sure? It is hard to decide, because the code compiles but it means nothing: §17.7 (8.3) is NDR, the compilers do not have to raise any compilation error.
The code is ill-formed, no diagnostic is required.
If std::is_signed_v<T>, then std::enable_if_t<std::is_signed_v<T>> denotes the type void. Otherwise, std::enable_if_t<std::is_signed_v<T>> does not denote a valid type. Therefore, every valid specialization of myAbs requires an empty template parameter pack.
Per [meta.rqmts]/4, the program has undefined behavior if std::enable_if is specialized. Therefore, the aforementioned behavior cannot be changed.
In my opinion IsSigned< T >... is a dependent template parameter,
therefore it can not be checked against §17.7 (8.3) in template
definition time. IsSigned< T > could be for example void for one
subset of Ts, int for another subset or substitution failure. For
the void subset it is true, that the empty template parameter pack
would be the only valid specialization, but the int subset could
have many valid specializations. It depends on the actual T
argument.
The compiler cannot check it, in the same way it cannot, say, solve an arbitrary equation for you. NDR (no diagnostic required) is made exactly for such cases — the program is ill-formed and would require a diagnostic if the compiler is actually capable of detecting that. NDR permits the compiler not to check it.
When N is zero, the instantiation of the expansion produces an empty
list. Such an instantiation does not alter the syntactic
interpretation of the enclosing construct.
The rule we are talking about is a semantic rule, not a syntactic rule, because syntactic rules are in [gram].
So what is the rationale for the NDR rules? In general, they address problems that are not reproducible among implementation strategies. For example, they may cause the code to misbehave in some implementation strategies, but do not cause any problems (and cannot be easily) in others.
Also, note that the standard talks in terms of program with terms like "ill-formed". Therefore, it is not always plausible to talk about the well-formed ness of an isolated code snippet. In this case, std::enable_if is required not to be specialized, but the situation may get more complicated otherwise.

Strange operator ?: usage with decltype

I am reading a book, that explains C++ traits and there is an example from C++ type_traits header with a strange ?: usage, here is the quote from the corresponding /usr/include/c++/... file:
template<typename _Tp, typename _Up>
static __success_type<typename decay<decltype
(true ? std::declval<_Tp>()
: std::declval<_Up>())>::type> _S_test(int);
Setting aside the purpose of the given declaration, the ?: operator usage puzzles me in this code. If the first operand is true, then std::declval<_Tp>() will always be chosen as result of the evaluation.
How does that declval operand selection actually works?
Edit: originally read in Nicolai M. Josuttis's "The C++ Standard Library: A Tutorial and Reference, 2nd ed.", p.125. But there it is given in a slightly simplified form as compared to what my GCC header files has.
In the expression true ? std::declval<_Tp>() : std::declval<_Up>() the first alternative is always selected, but the whole expression must be a valid expression. So std::declval<_Up>() must be valid and that means _Up must be a callable that accepts zero arguments. Beside that, _Tp() and _Up() must return the same type (or one of the types must be implicitly convertible to another), otherwise ternary iterator would not be able to select return value.
This technique is called SFINAE (substitution failure is not an error). The idea is that if template instantiation fails, then it is not an error, and this template is just ignored and compiler searches for another one.
The idea here is that ?: requires that the second and third operand has the same type, or one type is convertible to the other.
Otherwise the instantiation of the function will fail, and some other overload is selected.

Template argument deduction for variadic function pointer parameter - handling of ambiguous cases

Consider the following code:
#include <iostream>
void f(int) { }
void f(int, short) { }
template<typename... Ts> void g(void (*)(Ts...))
{
std::cout << sizeof...(Ts) << '\n';
}
template<typename T, typename... Ts> void h(void (*)(T, Ts...))
{
std::cout << sizeof...(Ts) << '\n';
}
int main()
{
g(f); // #1
g<int>(f); // #2
h(f); // #3
h<int>(f); // #4
}
The intent is to try each of the lines in the body of main() separately. My expectations were that all four calls were ambiguous and would result in compiler errors.
I tested the code on:
Clang 3.6.0 and GCC 4.9.2, both using -Wall -Wextra -pedantic -std=c++14 (-std=c++1y for GCC) - same behaviour in all these cases, except for minor differences in the wording of error messages;
Visual C++ 2013 Update 4 and Visual C++ 2015 CTP6 - again, same behaviour, so I'll call them "MSVC" going forward.
Clang and GCC:
#1: Compiler error, with a confusing message, basically no overload of 'f' matching 'void (*)()'. What? Where did the no-param declaration come from?
#3: Compiler error, with another confusing message: couldn't infer template argument 'T'. Of all the things that could fail there, deducing the argument for T would be the last one I would expect...
#2 and #4: Compiles with no errors and no warnings, and chooses the first overload.
For all four cases, if we eliminate one of the overloads (any one), the code compiles fine and chooses the remaining function. This looks like an inconsistency in Clang and GCC: after all, if deduction succeeds for both overloads separately, how can one be chosen over the other in cases #2 and #4? Aren't they both perfect matches?
Now, MSVC:
#1, #3 and #4: Compiler error, with a nice message: cannot deduce template argument as function argument is ambiguous. Now that's what I'm talking about! But, wait...
#2: Compiles with no errors and no warnings, and chooses the first overload. Trying the two overloads separately, only the first one matches. The second one generates an error: cannot convert argument 1 from 'void (*)(int,short)' to 'void (*)(int)'. Not so good anymore.
To clarify what I'm looking for with case #2, this is what the standard (N4296, first draft after C++14 final) says in [14.8.1p9]:
Template argument deduction can extend the sequence of template
arguments corresponding to a template parameter pack, even when the
sequence contains explicitly specified template arguments.
Looks like this part doesn't quite work in MSVC, making it choose the first overload for #2.
So far, it looks like MSVC, while not quite right, is at least relatively consistent. What's going on with Clang and GCC? What's the correct behaviour according to the standard for each case?
As far as I can tell, Clang and GCC are right in all four cases according to the standard, even though their behaviour may seem counter-intuitive, especially in cases #2 and #4.
There are two main steps in the analysis of the function calls in the code sample. The first one is template argument deduction and substitution. When that completes, it yields a declaration of a specialization (of either g or h) where all template parameters have been replaced with actual types.
Then, the second step attempts to match f's overloads against the actual pointer-to-function parameter that was constructed in the previous step. The best match is chosen according to the rules in [13.4] - Address of overloaded function; in our cases this is pretty simple, as there are no templates among the overloads, so we have either one perfect match or none at all.
The key point to understanding what happens here is that an ambiguity in the first step doesn't necessarily mean that the whole process fails.
The quotes below are from N4296 but the content hasn't changed since C++11.
[14.8.2.1p6] describes the process of template argument deduction when a function parameter is a pointer to function (emphasis mine):
When P is a function type, pointer to function type, or pointer to
member function type:
— If the argument is an overload set containing one or more function
templates, the parameter is treated as a non-deduced context.
— If the argument is an overload set (not
containing function templates), trial argument deduction is attempted
using each of the members of the set. If deduction succeeds for only
one of the overload set members, that member is used as the argument
value for the deduction. If deduction succeeds for more than one
member of the overload set the parameter is treated as a non-deduced
context.
For completeness, [14.8.2.5p5] clarifies that the same rule applies even when there's no match:
The non-deduced contexts are: [...]
— A function parameter for which argument deduction cannot be done because
the associated function argument is a function, or a set of overloaded
functions (13.4), and one or more of the following apply:
— more than one function matches
the function parameter type (resulting in an ambiguous deduction), or
— no function matches the function parameter type, or
— the set of functions supplied as an argument contains one or more
function templates.
So, no hard errors because of ambiguity in these cases. Instead, all template parameters are in non-deduced contexts in all our cases. This combines with [14.8.1p3]:
[...] A trailing template parameter pack (14.5.3) not otherwise
deduced will be deduced to an empty sequence of template arguments.
[...]
While the use of the word "deduced" is confusing here, I take this to mean that a template parameter pack is set to the empty sequence if no elements can be deduced for it from any source and there are no template arguments explicitly specified for it.
Now, the error messages from Clang and GCC start to make sense (an error message that only makes sense after you understand why the error occurs is not exactly the definition of a helpful error message, but I guess it's better than nothing):
#1: Since Ts is the empty sequence, the parameter of g's specialization is indeed void (*)() in this case. The compiler then tries to match one of the overloads to the destination type and fails.
#3: T only appears in a non-deduced context and is not explicitly specified (and it's not a parameter pack, so it cannot be "empty"), so a specialization declaration cannot be constructed for h, hence the message.
For the cases that do compile:
#2: Ts cannot be deduced, but one template parameter is explicitly specified for it, so Ts is int, making g's specialization's parameter void (*)(int). The overloads are then matched against this destination type, and the first one is chosen.
#4: T is explicitly specified as int and Ts is the empty sequence, so h's specialization's parameter is void (*)(int), the same as above.
When we eliminate one of the overloads, we eliminate the ambiguity during template argument deduction, so the template parameters are no longer in non-deduced contexts, allowing them to be deduced according to the remaining overload.
A quick verification is that adding a third overload
void f() { }
allows case #1 to compile, which is consistent with all of the above.
I suppose things were specified this way to allow template arguments involved in pointer-to-function parameters to be obtained from other sources, like other function arguments or explicitly-specified template arguments, even when template argument deduction can't be done based on the pointer-to-function parameter itself. This allows a function template specialization declaration to be constructed in more cases. Since the overloads are then matched against the parameter of the synthesized specialization, this means we have a way to select an overload even if template argument deduction is ambiguous. Quite useful if this is what you're after, terribly confusing in some other cases - nothing unusual, really.
The funny thing is that MSVC's error message, while apparently nice and helpful, is actually misleading for #1, somewhat but not quite helpful for #3, and incorrect for #4. Also, its behaviour for #2 is a side effect of a separate problem in its implementation, as explained in the question; if it weren't for that, it would probably issue the same incorrect error message for #2 as well.
This is not to say that I like Clang's and GCC's error messages for #1 and #3; I think they should at least include a note about the non-deduced context and the reason it occurs.

Why does the order of template argument substitution matter?

C++11
14.8.2 - Template Argument Deduction - [temp.deduct]
7 The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. The expressions include not only constant expressions such as those that appear in array bounds or as nontype template arguments but also general expressions (ie. non-constant expressions) inside sizeof, decltype, and other contexts that allow non-constant expressions.
C++14
14.8.2 - Template Argument Deduction - [temp.deduct]
7 The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. The expressions include not only constant expressions such as those that appear in array bounds or as nontype template arguments but also general expressions (ie. non-constant expressions) inside sizeof, decltype, and other contexts that allow non-constant expressions. The substitution proceeds in lexical order and stops when a condition that causes deduction to fail is encountered.
The added sentence explicitly states the order of substitution when dealing with template parameters in C++14.
The order of substitution is something that most often isn't given a lot of attention. I have yet to find a single paper on why this matters. Maybe this is because C++1y hasn't been fully standardized yet, but I'm assuming such a change must have been introduced for a reason.
The question:
Why, and when, does the order of template argument substitution matter?
As stated C++14 explicitly says that the order of template argument substitution is well-defined; more specifically it will be guaranteed to proceed in "lexical order and halt whenever a substitution causes the deduction to fail.
Compared to C++11 it will be much easier to write SFINAE-code that consists of one rule depending on another in C++14, we will also move away from cases where undefined ordering of template substitution can make our entire application suffer from undefined-behaviour.
Note: It's important to note that the behavior described in C++14 has always been the intended behavior, even in C++11, just that it hasn't been worded in such an explicit way.
What is the rationale behind such change?
The original reason behind this change can be found in a defect report originally submitted by Daniel Krügler:
C++ Standard Core Language Defect Reports and Accepted Issues, Revision 88
1227. Mixing immediate and non-immediate contexts in deduction failure
FURTHER EXPLANATION
When writing SFINAE we as developers depend on the compiler to find any substitution that would yield an invalid type or expression in our template when used. If such invalid entity is found we'd like to disregard whatever the template is declaring and move on to hopefully find a suitable match.
Substitution Failure Is Not An Error, but a mere.. "aw, this didn't work.. please move on".
The problem is that potential invalid types and expressions are only looked for in the immediate context of the substitution.
14.8.2 - Template Argument Deduction - [temp.deduct]
8 If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments.
[ Note: Access checking is done as part of the substitution process. --end note ]
Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.
[ Note: The evaluation of the substituted types and expressions can result in side effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such side effects are not in the "immediate context" and can result in the program being ill-formed. --end note]
In other words a substitution that occurs in a non-immediate context will still render the program ill-formed, which is why the order of template substitutions is important; it can change the whole meaning of a certain template.
More specifically it can be the difference between having a template which is usable in SFINAE, and a template which isn't.
SILLY EXAMPLE
template<typename SomeType>
struct inner_type { typedef typename SomeType::type type; };
template<
class T,
class = typename T::type, // (E)
class U = typename inner_type<T>::type // (F)
> void foo (int); // preferred
template<class> void foo (...); // fallback
struct A { };
struct B { using type = A; };
int main () {
foo<A> (0); // (G), should call "fallback "
foo<B> (0); // (H), should call "preferred"
}
On the line marked (G) we want the compiler to first check (E) and if that succeeds evaluate (F), but before the standard change discussed in this post there was no such guarantee.
The immediate context of the substitutions in foo(int) includes;
(E) making sure that the passed in T has ::type
(F) making sure that inner_type<T> has ::type
If (F) is evaluated even though (E) results in an invalid substitution, or if (F) is evaluated before (E) our short (silly) example won't make use of SFINAE and we will get an diagnostic saying that our application is ill-formed.. even though we intended for foo(...) to be used in such case.
Note: Notice that SomeType::type is not in the immediate context of the template; a failure in the typedef inside inner_type will render the application ill-formed and prevent the template from making use of SFINAE.
What implications will this have on code development in C++14?
The change will dramatically ease the life of language-lawyers trying to implement something which is guaranteed to be evaluated in a certain way (and order), no matter what conforming compiler they are using.
It will also make template argument substitution behave in a more natural way to non-language-lawyers; having the substitution occur from left-to-right is far more intuitive than erhm-like-any-way-the-compiler-wanna-do-it-like-erhm-....
Isn't there any negative implication?
The only thing I can think of is that since the order of substitution will occur from left-to-right a compiler is not permitted to handle multiple substitutions at once using an asynchronous implementation.
I have yet to stumble across such implementation, and I doubt that it would result in any major performance gain, but at least the thought (in theory) kinda fits on the "negative" side of things.
As an example: A compiler will not be able to use two threads that simultaneously does substitutions when instantating a certain template without any mechanism to act like the substitutions that occured after a certain point never happened, if that is required.
The story
Note: An example that could have been taken from real life will be presented in this section to describe when and why the order of template argument substitution matters. Please let me know (using the comment section) if anything is not clear enough, or maybe even wrong.
Imagine that we are working with enumerators and that we'd like a way to easily obtain the underlying value of the specified enumeration.
Basically we are sick and tired of always having to write (A), when we would ideally want something closer to (B).
auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)
auto value = underlying_value (SOME_ENUM_VALUE); // (B)
THE ORIGINAL IMPLEMENTATION
Said and done, we decide to write an implementation of underlying_value looking as the below.
template<class T, class U = typename std::underlying_type<T>::type>
U underlying_value (T enum_value) { return static_cast<U> (enum_value); }
This will ease our pain, and seems to do exactly what we want; we pass in an enumerator, and get the underlying value back.
We tell ourselves that this implementation is awesome and ask a colleague of ours (Don Quixote) to sit down and review our implementation before pushing it out into production.
THE CODE REVIEW
Don Quixote is an experienced C++ developer that has a cup of coffee in one hand, and the C++ standard in the other. It's a mystery how he manages to write a single line of code with both hands busy, but that's a different story.
He reviews our code and comes to the conclusion that the implementation is unsafe, we need to guard std::underlying_type from undefined-behaviour since we can pass in a T which is not of enumeration type.
20.10.7.6 - Other Transformations - [meta.trans.other]
template<class T> struct underlying_type;
Condition: T shall be an enumeration type (7.2)
Comments: The member typedef type shall name the underlying type of T.
Note: The standard specifies a condition for underlying_type, but it doesn't go any further to specifiy what will happen if it's instantiated with a non-enum. Since we don't know what will happen in such case the usage falls under undefined-behavior; it could be pure UB, make the application ill-formed, or order edible underwear online.
THE KNIGHT IN SHINING ARMOUR
Don yells something about how we always should honor the C++ standard, and that we should feel tremendous shame for what we have done.. it's unacceptable.
After he has calmed down, and had a few more sips of coffee, he suggests that we change the implementation to add protection against instantiating std::underlying_type with something which isn't allowed.
template<
typename T,
typename = typename std::enable_if<std::is_enum<T>::value>::type, // (C)
typename U = typename std::underlying_type<T>::type // (D)
>
U underlying_value (T value) { return static_cast<U> (value); }
THE WINDMILL
We thank Don for his discoveries and are now satisfied with our implementation, but only until we realize that the order of template argument substitution isn't well-defined in C++11 (nor is it stated when the substitution will stop).
Compiled as C++11 our implementation can still cause an instantiation of std::underlying_type with a T that isn't of enumeration type because of two reasons:
The compiler is free to evaluate (D) before (C) since the substitution order isn't well-defined, and;
even if the compiler evaluates (C) before (D), it's not guaranteed that it won't evaluate (D), C++11 doesn't have a clause explicitly saying when the substitution chain must stop.
The implementation by Don will be free from undefined-behavior in C++14, but only because C++14 explicitly states that the substitution will proceed in lexical order, and that it will halt whenever a substitution causes deduction to fail.
Don might not be fighting windmills on this one, but he surely missed a very important dragon in the C++11 standard.
A valid implementation in C++11 would need to make sure that no matter the order in which the substitution of template parameters occur the instantation of std::underlying_type won't be with an invalid type.
#include <type_traits>
namespace impl {
template<bool B, typename T>
struct underlying_type { };
template<typename T>
struct underlying_type<true, T>
: std::underlying_type<T>
{ };
}
template<typename T>
struct underlying_type_if_enum
: impl::underlying_type<std::is_enum<T>::value, T>
{ };
template<typename T, typename U = typename underlying_type_if_enum<T>::type>
U get_underlying_value (T value) {
return static_cast<U> (value);
}
Note: underlying_type was used because it's a simple way to use something in the standard against what is in the standard; the important bit is that instantiating it with a non-enum is undefined behavior.
The defect-report previously linked in this post uses a much more complex example which assumes extensive knowledge about the matter. I hope this story is a more suitable explanation for those who are not well read up on the subject.