In this blog post, I’ll show and explain a strange-looking error about tuple_size_v and instantiation for \n character. You’ll see some tricky parts of SFINAE and how the compiler builds the overload resolution set.

Let’s go.

A surprising error  

When doing experiments with tuple iteration (see part one and part two) I got this strange-looking compiler error:

error: incomplete type 'std::tuple_size<char>' used in nested name specifier

Something for a char??

This comes from the second line of the following snippet:

std::tuple tp { 10, 20, "hello"};
std::cout << tp << '\n';		  // << err ??

And it points to the custom operator<< for tuples:

template <typename TupleT, std::size_t TupSize = std::tuple_size_v<TupleT>>
std::ostream& operator <<(std::ostream& os, const TupleT& tp) {
    return printTupleImp(os, tp, std::make_index_sequence<TupSize>{}); 

In short, this function template takes a tuple and passes it to the printTupleImp function that does the job of printing all elements. We can assume that the code works fine, and the problem lies in the declaration of our operator <<.

See the “not working” code here @Compiler Explorer.

When I change:

TupSize = std::tuple_size_v<TupleT> into TupSize = std::tuple_size<TupleT>::value it works fine.

See here @Compiler Explorer.

What happens?  

To get a clear picture, we need to understand what’s happening here.

The line where we output a tuple:

std::cout << tp << '\n';

Expands (see at C++Insights) into two function calls to the operator <<:

operator<<(operator<<(std::cout, tp), '\n');

The nested call: operator<<(std::cout, tp) works fine and can correctly output the tuple.

But this one fails: operator<<(std::cout, '\n');.

Why does it fail?  

When the compiler tries to compile a function call (simplified):

  1. Perform a name lookup
  2. For function templates, the template argument values are deduced from the types of the actual arguments passed into the function.
    1. All occurrences of the template parameter (in the return type and parameters types) are substituted with those deduced types.
    2. When this process leads to an invalid type (like int::internalType) the particular function is removed from the overload resolution set. (SFINAE)
  3. At the end, we have a list of viable functions that can be used for the specific call.
    • If this set is empty, then the compilation fails.
    • If more than one function is chosen, we have an ambiguity.
    • In general, the candidate function whose parameters match the arguments most closely is the one that is called.

I wrote about this in my Notes on C++ SFINAE, Modern C++ and C++20 Concepts article.

For our case, the compiler tries to create a viable overload set for operator<<(std::cout, '\n');. So the problem lies somewhere in step 2.

Since our implementation for the custom operator << is in the global scope, the compiler has to include it and consider it when building the overload resolution set.

And here comes the problem:


For TupleT = char it doesn’t work.

It’s strange. I told you that when such an expression like int::internalType is invalid, the compiler can reject the code and don’t complain - Substitution Failure Is Not An Error (SFINAE).

However, this time, we have a bit different situation.

The key thing is the “immediate context” topic.

std::tuple_size_v<TupleT> is, in fact, a variable template with the following definition:

template <typename T>
inline constexpr size_t tuple_size_v = tuple_size<T>::value;

That means the compiler has to perform more steps, and it has to look inside the declaration of tuple_size_v and then check if the syntax fails.

On the other hand, when I use:

TupSize = std::tuple_size<TupleT>::value

Now, the compiler can immediately see if the expression std::tuple_size<char>::value is valid or not.

Here’s the implementation of tuple_size,

It uses some clever techniques for the implementation, but the key is that the instantiation fails when a given type is not a tuple.

Immediate context  

Let’s try and see the C++ Standard, N4868 - C++20 - General #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. [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] 9 A lambda-expression appearing in a function type or a template parameter is not considered part of the immediate context for the purposes of template argument deduction.

And also a good summary from @CppReference - SFINAE:

Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. A lambda expression is not considered part of the immediate context. (since C++20)

In our case, the substitution error happens in a variable template instantiation, and since it’s a side effect, we end up with a hard error.

More examples  

When looking for more examples I’ve found this one from “C++ Templates - The Complete Guide, 2nd Edition” (Link @Amazon). It’s from chapter 15th:

template <typename T> auto f(T p) {
    return p->m;

int f(...) { return 0; }

template <typename T>
auto g(T p) -> decltype(f(p)) {
    return 0;

int main() {
    return 0;                                        

Play @Compiler Explorer

We can see that when calling g(42), the compiler has to instantiate f<int>. In theory, we could expect that since auto f(T p) fails, then due to SFINAE the f(...) will be taken. Yet, this code break and won’t compile. Checking for the validity of p->m in the function body is not part of the immediate context, and thus the compiler can return an error here.

But if you add an additional overload:

auto f(int p) { return p; }

Then the code works! See here @Compiler Explorer

The compiler asks for f(int), and since there’s such an object, there’s no need to instantiate any further.

What else can be treated as not in immediate context? The book lists several things:

  • the definition of a class or function template, their “bodies.”
  • the initializer of a variable template (like our case with std::tuple_size_v)
  • and others like a default argument, a default member initializer, or an exception specification


SFINAE is tricky!

I spent lots of time figuring out and understanding why the strange error with operator<< and char even occurred. I expected that the compiler could use SFINAE, and as long as I have “fallback” functions, it should work nicely. Yet, the compiler has to stop at some point and generate a hard error if the failure happens in side effects and is not part of the immediate context of a function template.

It’s just a tip of an iceberg, but I hope you now have some intuition where to look for the source of errors in such tricky cases.

Back to you

Have you got into such a tricky SFINAE situation? Do you use SFINAE in your code? Share your feedback in the comments below.