1

Consider the following test code:

// Preprocessor
#include <iostream>
#include <type_traits>

// Structure with no type alias
template <class T>
struct invalid {
};

// Structure with a type alias
template <class T>
struct valid {
    using type = T;
};

// Traits getting the type of the first type
template <class T, class... Args>
struct traits {
    using type = typename T::type;
};

// One argument function
template <class T, class = typename traits<T>::type>
void function(T) {
    std::cout << "function(T)" << std::endl;
}

// Two arguments function
template <class T, class U, class = typename traits<T, U>::type>
void function(T, U) {
    std::cout << "function(T, U)" << std::endl;
}

// When function can be called on all arguments
template <
    class... Args,
    class = decltype(function(std::declval<Args>()...))
>
void sfinae(Args&&... args) {
    function(std::forward<Args>(args)...);
    std::cout << "sfinae(Args&&...)" << std::endl;
}

// When function can be called on all arguments except the first one
template <
    class T,
    class... Args,
    class = decltype(function(std::declval<Args>()...))
>
void sfinae(const invalid<T>&, Args&&... args) {
    function(std::forward<Args>(args)...);
    std::cout << "sfinae(const invalid<T>&, Args&&...)" << std::endl;
}

// Main function
int main(int argc, char* argv[]) {
    valid<int> v;
    invalid<int> i;
    sfinae(v);
    sfinae(i, v);
    return 0;
}

The code involves:

  • a structure invalid that has no ::type
  • a structure valid that has a ::type
  • a structure traits that defines ::type as T::type
  • an overloaded function which should work only if the type of the first argument is such that traits<T>::type is defined
  • an overloaded sfinae function that should be able to call function even if the first argument is invalid

However, the SFINAE mechanism does not seem to work in this instance, and I am not sure to understand why. The error is the following:

sfinae_problem_make.cpp:19:30: error: no type named 'type' in 'invalid<int>'
    using type = typename T::type;
                 ~~~~~~~~~~~~^~~~
sfinae_problem_make.cpp:29:46: note: in instantiation of template class 'traits<invalid<int>, valid<int> >' requested here
template <class T, class U, class = typename traits<T, U>::type>
                                             ^
sfinae_problem_make.cpp:30:6: note: in instantiation of default argument for 'function<invalid<int>, valid<int> >' required here
void function(T, U) {
     ^~~~~~~~~~~~~~~~
sfinae_problem_make.cpp:37:22: note: while substituting deduced template arguments into function template 'function' [with T = invalid<int>, U = valid<int>, $2 = (no value)]
    class = decltype(function(std::declval<Args>()...))
                     ^
sfinae_problem_make.cpp:39:6: note: in instantiation of default argument for 'sfinae<invalid<int> &, valid<int> &>' required here
void sfinae(Args&&... args) {
     ^~~~~~~~~~~~~~~~~~~~~~~~
sfinae_problem_make.cpp:60:5: note: while substituting deduced template arguments into function template 'sfinae' [with Args = <invalid<int> &, valid<int> &>, $1 = (no value)]
    sfinae(i, v);

The very surprising thing is that if traits is removed from the problem:

// Preprocessor
#include <iostream>
#include <type_traits>

// Structure with no type alias
template <class T>
struct invalid {
};

// Structure with a type alias
template <class T>
struct valid {
    using type = T;
};

// Traits getting the type of the first type
template <class T, class... Args>
struct traits {
    using type = typename T::type;
};

// One argument function
template <class T, class = typename T::type>
void function(T) {
    std::cout << "function(T)" << std::endl;
}

// Two arguments function
template <class T, class U, class = typename T::type>
void function(T, U) {
    std::cout << "function(T, U)" << std::endl;
}

// When function can be called on all arguments
template <
    class... Args,
    class = decltype(function(std::declval<Args>()...))
>
void sfinae(Args&&... args) {
    function(std::forward<Args>(args)...);
    std::cout << "sfinae(Args&&...)" << std::endl;
}

// When function can be called on all arguments except the first one
template <
    class T,
    class... Args,
    class = decltype(function(std::declval<Args>()...))
>
void sfinae(const invalid<T>&, Args&&... args) {
    function(std::forward<Args>(args)...);
    std::cout << "sfinae(const invalid<T>&, Args&&...)" << std::endl;
}

// Main function
int main(int argc, char* argv[]) {
    valid<int> v;
    invalid<int> i;
    sfinae(v);
    sfinae(i, v);
    return 0;
}

then it works as expected and outputs:

function(T)
sfinae(Args&&...)
function(T)
sfinae(const invalid<T>&, Args&&...)

Question: Why the first version does not work, and is there a way to make it work with the intermediary type traits?

Barry
  • 286,269
  • 29
  • 621
  • 977
Vincent
  • 57,703
  • 61
  • 205
  • 388

3 Answers3

3

SFINAE requires substitution failures to be "in the immediate context" of the instantiation. Otherwise a hard error will occur.

Without the intermediate traits type, the instantiation of function<invalid<int>, valid<int>, invalid<int>::type> causes an error in the immediate context because invalid<int> doesn't have a member named type, so SFINAE kicks in.

With the intermediate traits type, the error occurs during the instantiation of the definition traits<invalid<int>> since this requires the nonexistent invalid<int>::type. This is not in the immediate context, so a hard error occurs.

To fix this, you must ensure that traits always has a valid definition. This can be done like so:

template <class T, class = void>
struct traits {};

template <class T>
struct traits<T, std::void_t<typename T::type>> {
    using type = typename T::type;
};
Brian Bi
  • 111,498
  • 10
  • 176
  • 312
2

Fundamentally, this boils down to what "immediate context" means in [temp.deduct]/8, the sfinae rule, which isn't super clearly defined (see cwg 1844):

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: 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: 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 ]

In this case, arguably the immediate context is just to see that traits<T,U>::type is a thing that exists. Which it does. But it's only when we go through and instantiate that type as the default argument that we have to look at what T::type is. But that's a little delayed from what we actually need.

What you need is to either force the instantiation of traits itself to fail or force traits to not have a member alias named type if T does not. The cop-out short version would just be:

template <class T, class... Args>
struct traits;

template <class T>
struct traits<valid<T>> {
    using type = T;
};

But you'll want something slightly more robust than that.


Unfortunately, you cannot add a trailing defaulted template argument like:

template <typename T, typename... Args, typename = typename T::type>
struct traits {
    using type = typename T::type;
};

due to [temp.param]/15, but with Concepts, you could do:

template <typename T>
concept Typed = requires {
    typename T::type;
};

template <Typed T, typename... Args>
struct traits {
    using type = typename T::type;
};
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Corollary question: is there a legitimate reason for the "immediate context" thing in the standard (for example would open a pandora box and lead to bad things), or is it just a historical thing with no good technical motivation? – Vincent Aug 29 '18 at 21:26
  • @Vincent Avoiding arbitrary compiler complexity probably. – Barry Aug 29 '18 at 22:16
1

If you read the description of SFINAE, there is this sentence:

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.

That traits<T, U>::type is accessed within the immediate context of function and not that of sfinae. This is why it results in a compiler error.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271