5

I'm trying to combine the approaches used in this answer for detecting whether a class has a member variable x with this answer to select different implementations depending on that using enable_if.

Basically, I want to write a trait class that, given a type T, provides access to the member T::x if it exists, and provides a default value otherwise.

The following code does not compile on g++: (Compiler Explorer)

#include <iostream>
#include <type_traits>

// classes with / without x member
struct WithX { static constexpr int x = 42; };
struct WithoutX {};

// trait to detect x
template <typename T, typename = void>
struct HasX : std::false_type { };

template <typename T>
struct HasX <T, decltype((void) T::x)> : std::true_type { };

// trait to provide default for x
template <typename T>
struct FooTraits
{
    template <bool enable = HasX<T>::value>
    static constexpr std::enable_if_t< enable, size_t> x() { return T::x; }
    template <bool enable = HasX<T>::value>
    static constexpr std::enable_if_t<!enable, size_t> x() { return 1; }

};


int main() {
    std::cout << HasX<WithX>::value << std::endl;
    // Uncomment the following line to make this compile with g++
    //std::cout << HasX<WithoutX>::value << std::endl;
    std::cout << FooTraits<WithoutX>::x() << std::endl;
}

g++ gives error messages that

error: 'x' is not a member of 'WithoutX'
  struct HasX <T, decltype((void) T::x)> : std::true_type { };

in the part which should detect whether x is a member in the first place. Curiously though, if I uncomment the second to last line which instantiates HasX<WithoutX>::value by itself, g++ compiles without errors (Compiler Explorer).

Both clang and msvc compile without a problem on Compiler Explorer.

What's wrong here?

Andre
  • 675
  • 4
  • 19

2 Answers2

3

SFINAE only work in immediate context. In other words, if the compiler can see in advance that a declaration has a problem, then it must be an error.

When instantiating a class, the compiler will try to resolve everything it can. So is this:

template<bool enable = HasX<T>::value>
....

This is not dependent of the context of the function. This can be instantiated right when FooTraits is instantiated.

In other words, this assignation can be calculated in advance, as if you'd move it to the class scope.

In your case, the compiler has nothing to do at subtitution.

The fix is simply that:

template <typename U = T, bool enable = HasX<U>::value>
static constexpr std::enable_if_t< enable, size_t> x() { return T::x; }
template <typename U = T, bool enable = HasX<U>::value>
static constexpr std::enable_if_t<!enable, size_t> x() { return 1; }

In theory, U could be a completely different type. U is immediate to the function instantiation, while T is not.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • That suggestion works, thanks. So which compiler is wrong, g++ or clang/msvc? – Andre Mar 14 '19 at 08:18
  • Issue is not in `FooTraits`, as using `void_t` for `HasX` fix in both compiler [Demo](http://coliru.stacked-crooked.com/a/93be7563a671a0fc). – Jarod42 Mar 14 '19 at 10:49
1

Fact that switching comment of:

//std::cout << HasX<WithoutX>::value << std::endl;

is indeed a good sign of gcc bug.

Seems gcc has issue with your form:

template <typename T>
struct HasX <T, decltype((void) T::x)> : std::true_type {};

A more typical way is to use std::void_t:

template <typename T>
struct HasX <T, std::void_t<decltype(T::x)>> : std::true_type {};

which indeed solves the issue Demo.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Interesting. This variant works with gcc 5+, but fails (with and without the commented line) for 4.9.4, which compiled depending on the comment line. – Andre Mar 14 '19 at 15:22
  • That problem seems to be related to CWG1558. With the more complex definition of `void_t` as explained here https://en.cppreference.com/w/cpp/types/void_t it also works on that gcc. – Andre Mar 14 '19 at 15:30