Reputation: 60381
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:
invalid
that has no ::type
valid
that has a ::type
traits
that defines ::type
as T::type
function
which should work only if the type of the first argument is such that traits<T>::type
is definedsfinae
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?
Upvotes: 1
Views: 81
Reputation: 303147
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;
};
Upvotes: 2
Reputation: 119239
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;
};
Upvotes: 3
Reputation: 136296
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.
Upvotes: 1