15

With regards to the following code (https://wandbox.org/permlink/nhx4pheijpTF1ohf reproduced below for convenience)

#include <type_traits>
#include <utility>

namespace foo_name {
template <typename T>
void foo();
template <>
void foo<int>();

template <typename T>
struct c_size;
template <>
struct c_size<int> : public std::integral_constant<int, 1> {};
} // namespace foo_name

template <typename Type>
class Foo {
public:
    template <typename T>
    static decltype(auto) impl(T&& t) {
        using foo_name::foo;
        return foo(std::forward<T>(t));
    }
};

class Something {};

template <typename Type, typename T = std::decay_t<Type>>
using EnableIfHasFoo = std::void_t<
    decltype(Foo<T>::impl(std::declval<T>())),
    decltype(foo_name::c_size<Type>::value)>;

template <typename Type, typename = std::void_t<>>
class Test {};
template <typename Type>
class Test<Type, EnableIfHasFoo<Type>> {};

int main() {
    static_cast<void>(Test<Something>{});
}

The code above exits with an error because the instantiation of Foo<T>::impl() causes a hard error and is not usable in a SFINAE context. But the strange thing here is that when you switch the order of things in the void_t in EnableIfHasFoo (to the following https://wandbox.org/permlink/at1KkeCraNwHGmUI), it will compile

template <typename Type, typename T = std::decay_t<Type>>
using EnableIfHasFoo = std::void_t<
    decltype(foo_name::c_size<Type>::value),
    decltype(Foo<T>::impl(std::declval<T>()))>;

Now the questions are

  1. Why does the code initially not compile? The instantiation of Foo<T>::impl() is in context of the substitution, so it should work?
  2. Substituting foo_name::foo(T) in place of the first argument to void_t will make it compile (see https://wandbox.org/permlink/g3NaPFZxdUPBS7oj), why? How does adding one extra layer of indirection make the situation different?
  3. Why does the order in void_t make a difference, does the compiler short circuit the expressions within the type pack?
Curious
  • 20,870
  • 8
  • 61
  • 146

1 Answers1

7

1) and 2) have the same answer; SFINAE does not work with return type deduction since the body of a function is not in immediate context:

10 - Return type deduction for a function template with a placeholder in its declared type occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand. [ Note: Therefore, any use of a specialization of the function template will cause an implicit instantiation. Any errors that arise from this instantiation are not in the immediate context of the function type and can result in the program being ill-formed (17.8.2). — end note ]

3) is a more interesting question; the short-circuiting is intentional, and is guaranteed by [temp.deduct]:

7 - [...] The substitution proceeds in lexical order and stops when a condition that causes deduction to fail is encountered.

This short-circuiting works for gcc, clang and ICC, but unfortunately MSVC (as of CL 19 2017 RTW) gets it wrong, for example:

template<class T> auto f(T t) -> decltype(t.spork) { return t.spork; }
template<class T> auto g(T t) { return t.spork; }
int x(...);
template<class...> using V = void;
template<class T> auto x(T t) -> V<decltype(f(t)), decltype(g(t))> {}
int a = x(0);
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 1
    Ah. Very interesting! Before I accept this. I feel like i get the rules around "immediate context" wrong a lot. Could you link me to a resource or something where i can learn what exactly this immediate context is? – Curious Aug 09 '17 at 14:55
  • Also how why does this code compile when there is no extra layer if indirection. i.e. My second question. Is that because of return type deduction? If Foo::impl() had a specified return type. Would this code work? – Curious Aug 09 '17 at 14:58
  • @Curious https://stackoverflow.com/questions/15260685/what-exactly-is-the-immediate-context-mentioned-in-the-c11-standard-for-whic is quite helpful, I think. If `Foo::impl()` has a specified return type, that return type is in the immediate context so is covered by SFINAE. – ecatmur Aug 09 '17 at 15:02
  • One last followup, why is it that when I supply the return type of `Foo::impl` in my code, it compiles? Isn't `foo_name::foo` still out of the current context since it deduces return types? – Curious Aug 09 '17 at 16:21
  • @Curious it's not what it does (calculating a return type) that matters, its where it happens (in the function template declaration as opposed to the function template body). The function template instantiation is what makes the return type deduction occur outside the immediate context in your original case. – ecatmur Aug 09 '17 at 16:45
  • But even if i replace the body with a trailing return type. Isnt the return type going to be deduced in the body of foo_name::foo() anyway? – Curious Aug 09 '17 at 16:46
  • @Curious `foo_name::foo` is specified as `template void foo();` so there's no need to instantiate it to find out its return type. – ecatmur Aug 09 '17 at 17:16
  • Ah, yes I got confused with another example I wrote before asking this question. So if `foo_name::foo` had an `auto` return type it would trigger an error right? – Curious Aug 09 '17 at 17:42
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151529/discussion-between-ecatmur-and-curious). – ecatmur Aug 09 '17 at 18:26