What Exactly Is the "Immediate Context" Mentioned in the C++11 Standard For Which Sfinae Applies

What exactly is the immediate context mentioned in the C++11 Standard for which SFINAE applies?

If you consider all the templates and implicitly-defined functions that are needed to determine the result of the template argument substitution, and imagine they are generated first, before substitution starts, then any errors occurring in that first step are not in the immediate context, and result in hard errors.

If all those instantiations and implicitly-definitions (which might include defining functions as deleted) can be done without error, then any further "errors" that occur during substitution (i.e. while referring to the instantiated templates and implicitly-defined functions in the function template's signature) are not errors, but result in deduction failures.

So given a function template like this:

template<typename T>
void
func(typename T::type* arg);

and a "fall-back" that will be used if deduction fails for the other function:

template<typename>
void
func(...);

and a class template like this:

template<typename T>
struct A
{
typedef T* type;
};

A call to func<A<int&>>(nullptr) will substitute A<int&> for T and in order to check if T::type exists it must instantiate A<int&>. If we imagine putting an explicit instantiation before the call to func<A<int&>(nullptr):

template class A<int&>;

then that would fail, because it tries to create the type int&* and pointers to references are not allowed. We don't get to the point of checking if substitution succeeds, because there is a hard error from instantiating A<int&>.

Now let's say there's an explicit specialization of A:

template<>
struct A<char>
{
};

A call to func<A<char>>(nullptr) requires the instantiation of A<char>, so imagine an explicit instantiation somewhere in the program before the call:

template class A<char>;

This instantiation is OK, there's no error from this, so we proceed to argument substitution. The instantiation of A<char> worked, but A<char>::type doesn't exist, but that's OK because it's only referenced in the declaration of func, so only causes argument deduction to fail, and the fall-back ... function gets called instead.

In other situations substitution might cause special member functions to be implicitly-defined, possibly as deleted, which might trigger other instantiations or implicit definitions. If errors occur during that "generating instantiations and implicit definitions" stage then they're errors, but if that succeeds but during substitution an expression in the function template signature turns out to be invalid e.g. because it uses a member that doesn't exist or something that got implicitly defined as deleted, that's not an error, just a deduction failure.

So the mental model I use is that substitution needs to do a "preparation" step first to generate types and members, which might cause hard errors, but once we have all the necessary generation done, any further invalid uses are not errors. Of course all this does is move the problem from "what does immediate context mean?" to "Which types and members need to be generated before this substitution can be checked?" so it may or may not help you!

Concept requirement and non-immediate context

Firstly, as you referred, negate<non_negatable> is not a substitution failure since the error is not in the immediate context of the function, as such it's ill-formed. From 13.10.3.1/8 of C++20 standard:

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.

[Note 4: If no diagnostic is required, the program is still ill-formed.
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, its template parameter types, and its explicit-specifier can result in a deduction failure.

[Note 5: The substitution into types and expressions can result in effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such effects are not in the “immediate context” and can result in the program being ill-formed.
— end note]

Secondly, 7.5.7.1/6 says:

[...]

[Note 1: If a requires-expression contains invalid types or expressions in its requirements, and it does not appear within the declaration of a templated entity, then the program is ill-formed.
— end note]

[...]

So, your understanding seems correct to me. However, what you're asking in your last question boils down to whether there is a way to make an invalid expression in a template function body cause a substitution failure instead of an ill-formed program. I don't think it's possible.

Confusion with hard error in SFINAE

1) and 2) have the same answer; SFINAE does not work with return type deduction since the body of a function is not in immediate context:

10 - Return type deduction for a function template with a placeholder in its declared type occurs when the
definition is instantiated even if the function body contains a return statement with a non-type-dependent
operand. [ Note: Therefore, any use of a specialization of the function template will cause an implicit
instantiation. Any errors that arise from this instantiation are not in the immediate context of the function
type and can result in the program being ill-formed (17.8.2). — end note ]

3) is a more interesting question; the short-circuiting is intentional, and is guaranteed by [temp.deduct]:

7 - [...] The substitution proceeds
in lexical order and stops when a condition that causes deduction to fail is encountered.

This short-circuiting works for gcc, clang and ICC, but unfortunately MSVC (as of CL 19 2017 RTW) gets it wrong, for example:

template<class T> auto f(T t) -> decltype(t.spork) { return t.spork; }
template<class T> auto g(T t) { return t.spork; }
int x(...);
template<class...> using V = void;
template<class T> auto x(T t) -> V<decltype(f(t)), decltype(g(t))> {}
int a = x(0);

Is `sizeof(T)` with an incomplete type a valid substitution-failure as per the C++ Standard?

"Shall not be applied" means that it would normally be ill-formed. In an SFINAE context, if something would normally be ill-formed due to resulting in "an invalid type or expression", this becomes a substitution failure, as long as it is in the "immediate context" (C++20 [temp.deduct]/8) and not otherwise excluded from SFINAE (e.g. see p9 regarding lambda expressions).

There is no difference between "invalid" and "ill-formed" in this context. p8 explicitly says: "An invalid type or expression is one that would be ill-formed, with a diagnostic required, if written using the substituted arguments." This wording has been present since C++11. However, in C++03, invalid expressions were not substitution failures. This is the famous "expression SFINAE" feature that was added in C++11, after compiler implementers were sufficiently convinced that they would be able to implement it.

There is no rule in the standard that says that sizeof expressions are an exception to the SFINAE rules, so as long as an invalid sizeof expression occurs in the immediate context, SFINAE applies.

The "immediate context" has still not been explicitly defined in the standard. An answer by Jonathan Wakely, a GCC dev, explains the intent. Eventually, someone might get around to formally defining it in the standard.

However, the case of incomplete types, the problem is that this technique is very dangerous. First, if the completeness check is performed twice in the same translation unit on the same type, the instantiation is only performed once; this implies that the second time it's checked, the result of the check will still be false, because the is_type_complete_v<T> will simply refer to the previous instantiation. Chen's post appears to simply be wrong about this: GCC, Clang, and MSVC all behave the same way. See godbolt. It's possible that the behaviour was different on an older version of MSVC.

Second, if there is cross-translation-unit variance: that is, is_type_complete_v<T> is instantiated in one translation unit and is false, and is instantiated in another translation unit and is true there, the program is ill-formed NDR. See C++20 [temp.point]/7.

For this reason, completeness checks are generally not done; instead, library implementers either say that you are allowed to pass incomplete types to their templates and they will work properly, or that you must pass a complete type but the behaviour is undefined if you violate this requirement, as it cannot be reliably checked at compile time.

One creative way around the template instantiation rules is to use a macro with __COUNTER__ to make sure that you have a fresh instantiation every time you use the type trait, and you have to define the is_type_complete_v template with internal linkage, to avoid the issue of cross-TU variance. I got this technique from this answer. Unfortunately, __COUNTER__ is not in standard C++, but this technique should work on compilers that support it.

(I looked into whether the C++20 source_location feature can replace the non-standard __COUNTER__ in this technique. I think it can't, because IS_COMPLETE may be referenced from the same line and column but within two different template instantiations that somehow both decide to check the same type, which is incomplete in one and complete in the other.)

How function overload lookup works when one of the functions does not compile?

Only errors from the immediate context cause a "soft" error that removes the function template from the overload set. The compiler must instantiate base_type<T> before it can evaluate sizeof(base_type<T>), and any errors resulting from that instantiation are not in the immediate context, and cause hard errors.

I am not sure what you are really trying to do, but you can probably do it using std::enable_if_t<std::is_union_v<T>> to disable the overload. The reason why this works is that the instantiation of std::enable_if which is done first causes no errors; the resulting class simply may not contain a member named type. The access to type is in the immediate context.



Related Topics



Leave a reply



Submit