Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ SFINAE with decltype: substitution failure becomes an error?

Tags:

c++

sfinae

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.

like image 248
Leedehai Avatar asked Aug 26 '17 03:08

Leedehai


Video Answer


2 Answers

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; }
};

Demo (C++11)


What's happening

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 fromstd::false_typeif the expression is well-formed but it's not constructibe, andtrue_type` otherwise.

like image 62
AndyG Avatar answered Sep 20 '22 18:09

AndyG


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.

like image 20
Guillaume Racicot Avatar answered Sep 22 '22 18:09

Guillaume Racicot