Is the "lazy man's enable_if" legal C++?

[temp.over.link]/6 specifies when two function template declarations are overloads. That is done by defining equivalency of two function templates as follows:

Two function templates are equivalent if they [..] have return types [..] that are equivalent using the rules described above to compare expressions involving template parameters.

The "rules described above" are

Two expressions involving template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one definition rule (3.2) [..]

The ODR relevant for this part states in [basic.def.odr]/6 that

Given such an entity named D defined in more than one translation unit, then

  • each definition of D shall consist of the same sequence of tokens;

Clearly, as the return types (which are the trailing return types as per [dcl.fct]/2) do not consist of the same tokens, two function definitions containing those expressions would violate the ODR.
Hence the declarations of foo declare non-equivalent function templates and overload the name.

The error you see is issued due to the lack of support from VC++ for expression SFINAE - presumably the trailing-return-types are not inspected for equivalency.


Workaround

You can make the function templates non-equivalent in another way - Change the template parameter list. If you rewrite the second definition like so:

template <typename F, int=0>
auto foo(F&& f) -> decltype(f(0, 1), void())
{
    std::cout << "2" << std::endl;
}

Then VC++ compiles it fine. I shortened the quote in [temp.over.link]/6, which covers this:

Two function templates are equivalent if they are declared in the same scope, have the same name, have identical template parameter lists [..]

In fact, to be able to easily introduce new overloads, you can use a little helper:

template <int I>
using overload = std::integral_constant<int, I>*;

Usage is e.g.

// Remember to separate > and = with whitespace
template <typename... F, overload<0> = nullptr>
auto foo(F&&... f) -> decltype(f(0, 1)..., void())

template <typename... F, overload<1> = nullptr>
auto foo(F&&... f) -> decltype(f(0, 1, 2)..., void())

Demo.


This is a feature called "Expression SFINAE." It is not yet fully supported by Visual C++ (see "C++11/14/17 Features In VS 2015 Preview" for the latest conformance update as of the time of this answer).