2

I have distilled the issue down to the following class which is attempting to use std::enable_if to disable a member function:

#include <type_traits>

int foo(int& x) {
    return x;
}

template<bool enable>
struct A {
int x;
template<bool dummy=true,
        typename Enabler = typename std::enable_if<dummy && enable>::type>
 auto disabled_method() const -> decltype(foo(x)) {

    return foo(x);
 }
};

int main() {
    A<false> x;
}

There is a type error in the expression decltype(foo(x)), even though the function should be disabled by the enable_if!

Note that it is particular to occuring in the return type of the function. For example, if we move the decltype(foo(x)) to the body of the function, SFINAE will work fine:

template<bool dummy=true,
        typename Enabler = typename std::enable_if<dummy && enable>::type>
 auto disabled_method() const -> int {
    decltype(foo((X)x)) local;
    return foo(x);
 }

(Same for using decltype(foo((X)x)) as an argument type)

Likewise, it will correctly perform SFINAE if I declare it as

template<bool dummy=true,
        typename X = const int,
        typename Enabler = typename std::enable_if<dummy && enable>::type>
 auto disabled_method() const -> decltype(foo((X)x)) {

    return foo(x);
 }

But using decltype(foo((const int)x)), it will error out, despite X being equal to const int above. This seems to indicate that introducing the extra cast to the template parameter X causes it to delay the substitution.

But the dummy and Enabler template pattern up above is supposed to do that anyways?

Why is this happening?

Jeremy Salwen
  • 8,061
  • 5
  • 50
  • 73

2 Answers2

4

This has nothing to do with "immediate context".

In general, errors in nondependent constructs render the program ill-formed NDR (no diagnostic required), see [temp.res]/8. Implementations have broad leeway to diagnose - or not diagnose - such problems.

In your case, foo(x) doesn't depend on a template parameter, and is always ill-formed; implementations are therefore free to diagnose it even if A is never instantiated.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • 1
    If `foo(x)` does not depend on a template parameter then it is not in the immediate context? – Curious Jun 22 '17 at 04:44
  • @Curious That conflates two distinct sets of rules. In particular, it implies that the compiler can't ever treat such things as substitution failure. – T.C. Jun 22 '17 at 06:17
1

The return type is a part of the SFINAE process, this is why you can also include the std::enable_if as a part of the return type. But SFINAE only works if there is an error in the immediate context, the foo(x) however is not a part of the immediate template context, therefore the compiler throws an error.

For example, if you change the code to be the following

template<bool dummy=true>
std::enable_if_t<dummy && enable, decltype(foo(x))> disabled_method()
        const {
    return foo(x);
}

Then the enable_if works, because not the decltype(foo(x)) is a part of the enable_if itself and outside the immediate context where x is non-const

Take a look at this question and its answer for more information about what the immediate context is What exactly is the "immediate context" mentioned in the C++11 Standard for which SFINAE applies?


For your convenience, a quote from the linked question's answer that explains what the immediate context is

If you consider all the templates and implicitly-defined functions that are needed to determine the result of the template argument substitution, and imagine they are generated first, before substitution starts, then any errors occurring in that first step are not in the immediate context, and result in hard errors.

In your example, the return type is needed to determine the result of the template argument substitution and is independent of the substitution process, hence it results in a hard error.


Also in such cases, I usually find it easier to leave out the return type entirely, and let the compiler deduce it. It's much easier to work with.

Curious
  • 20,870
  • 8
  • 61
  • 146
  • 2
    Actually, the example you posted only works because x is non-const outside the context of the function definition. This is clear if you switch the argument type of foo() to std::string. (But I do know that you can get it to work if you use boost::lazy_enable_if). I will take a closer look at your references, thank you! – Jeremy Salwen Jun 22 '17 at 04:06
  • I don't believe that "immediate context" explains this case, because the example with `foo((X)x)` is in exactly the same context, but works. It seems to depend on whether the template parameters are used, not on the context. – Jeremy Salwen Jun 22 '17 at 06:27
  • @JeremySalwen `foo((X)x)` works because it is dependent on the template parameters which puts it in the immediate context of deduction, anything else is not in the immediate context. – Curious Jun 22 '17 at 07:05