This code works:
// Code A
#include <iostream>
#include <vector>
#include <type_traits>
using namespace std;
template <typename T>
struct S {
template <typename Iter, typename = typename enable_if<is_constructible<T, decltype(*(declval<Iter>()))>::value>::type>
S(Iter) { cout << "S(Iter)" << endl; }
S(int) { cout << "S(int)" << endl; }
};
int main()
{
vector<int> v;
S<int> s1(v.begin()); // stdout: S(Iter)
S<int> s2(1); // stdout: S(int)
}
But this code below doesn't work. In the code below, I merely want to inherit std::enable_if
, so the class is_iter_of
will have member typedef type
if the selected version of std::enable_if
has member typedef type
.
// Code B
#include <iostream>
#include <vector>
#include <type_traits>
using namespace std;
template <typename Iter, typename Target>
struct is_iter_of : public enable_if<is_constructible<Target, decltype(*(declval<Iter>()))>::value> {}
template <typename T>
struct S {
template <typename Iter, typename = typename is_iter_of<Iter, T>::type>
S(Iter) { cout << "S(Iter)" << endl; }
S(int) { cout << "S(int)" << endl; }
};
int main()
{
vector<int> v;
S<int> s1(v.begin());
S<int> s2(1); // this is line 22, error
}
Error message:
In instantiation of 'struct is_iter_of<int, int>':
12:30: required by substitution of 'template<class Iter, class> S<T>::S(Iter) [with Iter = int; <template-parameter-1-2> = <missing>]'
22:16: required from here
8:72: error: invalid type argument of unary '*' (have 'int')
The error message is bewildering: of course I want the template substitution to fail.. so the correct constructor could be selected. Why didn't SFINAE work in Code B
? If invalid type argument of unary '*' (have 'int')
offends the compiler, the compiler should have issued a same error for Code A
as well.
The problem is that the expression *int
(*(declval<Iter>())
) is invalid, so your template fails. You need to another level of templating, so I suggest a void_t approach:
make is_iter_of
by a concept-lite that derives from true_type
or fals_type
use enable_if
within the definition of your class to enable the iterator constructor.
The key thing to understand is that your constructor before needed a type for typename is_iter_of<Iter, T>::type
except that your enable_if
in the struct is_iter_of
caused the entire thing to be ill-formed. And since there was no fall-back template you had a compiler error.
template<class...>
using voider = void;
template <typename Iter, typename Target, typename = void>
struct is_iter_of : std::false_type{};
template <typename Iter, typename Target>
struct is_iter_of<Iter, Target, voider<decltype(*(declval<Iter>()))>> : std::is_constructible<Target, decltype(*(declval<Iter>()))> {};
template <typename T>
struct S {
template <typename Iter, typename std::enable_if<is_iter_of<Iter, T>::value, int>::type = 0>
S(Iter) { cout << "S(Iter)" << endl; }
S(int) { cout << "S(int)" << endl; }
};
The additional voider
makes the template specialization not preferred if *(declval<Iter>())
is an ill-formed expression (*int
) and so the fallback base template (std::false_type
) is chosen.
Else, it will derived from std::is_constructible``. In other words, it can still derive from
std::false_typeif the expression is well-formed but it's not constructibe, and
true_type` otherwise.
The thing is you're trying to extend from std::enable_if
, but the expression you put inside the enable if may be invalid. Since you are using a class that inherit form that, the class you instanciate inherit from an invalid expression, hence the error.
An easy solution for having a name for your enable_if
expression would be to use an alias instead of a class:
template <typename Iter, typename Target>
using is_iter_of = enable_if<is_constructible<Target, decltype(*(declval<Iter>()))>::value>;
SFINAE will still work as expected with an alias.
This is because instancing an alias is part of the function you try to apply SFINAE on. With inheritance, the expression is part of the class being instanciated, not the function. This is why you got a hard error.
The thing is, the is multiple ways SFINAE is applied in your case. Let's take a look at where SFINAE can happen:
enable_if< // here -------v
is_constructible<Target, decltype(*(declval<Iter>()))>::value
>::type
// ^--- here
Indeed, SFINAE will happen because enable_if::type
will not exist if the bool parameter is false, causing SFINAE.
But if you look closely, another type might not exist: decltype(*(std::declval<Iter>()))
. If Iter
is int
, asking for the type of the star operator makes no sense. So SFINAE if applied there too.
Your solution with inheritance would have work if every class you send as Iter
had the *
operator available. Since with int
it does not exist, you are sending a non existing type to std::is_constructible
, making the whole expression forming the base class invalid.
With the alias, the whole expression of using std::enable_if
is subject to apply SFINAE. Whereas the base class approach will only apply SFINAE on the result of std::enable_if
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With