Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SFINAE: Detecting existence of member variable does not work on g++

Tags:

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?

like image 792
Andre Avatar asked Mar 13 '19 12:03

Andre


2 Answers

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.

like image 155
Guillaume Racicot Avatar answered Nov 15 '22 04:11

Guillaume Racicot


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.

like image 27
Jarod42 Avatar answered Nov 15 '22 04:11

Jarod42