Is the “lazy man's enable_if” legal C++?

2019-03-08 17:39发布

问题:

I frequently use a technique I call the "lazy man's enable_if," where I use decltype and the comma operator to enable a function based on some template input. Here is a small example:

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

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

With --std=c++11, g++ 4.7+ and Clang 3.5+ happily compile that bit of code (and it works as I would expect). However, when using MSVC 14 CTP5, I get this error complaining of foo already being defined:

Error error C2995: 'unknown-type foo(F &&)': function template has already been defined c++-scratch main.cpp 15

So my question is: Is the "lazy man's enable_if" legal C++ or is this an MSVC bug?

回答1:

[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.



回答2:

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).