How to detect whether there is a specific member variable in class?

Kirill V. Lyadvinsky picture Kirill V. Lyadvinsky · Jun 17, 2009 · Viewed 36.7k times · Source

For creating algorithm template function I need to know whether x or X (and y or Y) in class that is template argument. It may by useful when using my function for MFC CPoint class or GDI+ PointF class or some others. All of them use different x in them. My solution could be reduces to the following code:


template<int> struct TT {typedef int type;};
template<class P> bool Check_x(P p, typename TT<sizeof(&P;::x)>::type b = 0) { return true; }
template<class P> bool Check_x(P p, typename TT<sizeof(&P;::X)>::type b = 0) { return false; }

struct P1 {int x; };
struct P2 {float X; };
// it also could be struct P3 {unknown_type X; };

int main()
{
    P1 p1 = {1};
    P2 p2 = {1};

    Check_x(p1); // must return true
    Check_x(p2); // must return false

    return 0;
}

But it does not compile in Visual Studio, while compiling in the GNU C++. With Visual Studio I could use the following template:


template<class P> bool Check_x(P p, typename TT<&P;::x==&P;::x>::type b = 0) { return true; }
template<class P> bool Check_x(P p, typename TT<&P;::X==&P;::X>::type b = 0) { return false; }

But it does not compile in GNU C++. Is there universal solution?

UPD: Structures P1 and P2 here are only for example. There are could be any classes with unknown members.

P.S. Please, do not post C++11 solutions here because they are obvious and not relevant to the question.

Answer

Cassio Neri picture Cassio Neri · Apr 14, 2013

Here is a solution simpler than Johannes Schaub - litb's one. It requires C++11.

#include <type_traits>

template <typename T, typename = int>
struct HasX : std::false_type { };

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

Update: A quick example and the explanation on how this works.

For these types:

struct A { int x; };
struct B { int y; };

we have HasX<A>::value == true and HasX<B>::value == false. Let's see why.

First recall that std::false_type and std::true_type have a static constexpr bool member named value which is set to false and true, respectively. Hence, the two templates HasX above inherit this member. (The first template from std::false_type and the second one from std::true_type.)

Let's start simple and then proceed step by step until we get to the code above.

1) Starting point:

template <typename T, typename U>
struct HasX : std::false_type { };

In this case, there's no surprise: HasX derives from std::false_type and hence HasX<bool, double>::value == false and HasX<bool, int>::value == false.

2) Defaulting U:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

Given that U defaults to int, Has<bool> actually means HasX<bool, int> and thus, HasX<bool>::value == HasX<bool, int>::value == false.

3) Adding a specialization:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

// Specialization for U = int
template <typename T>
struct HasX<T, int> : std::true_type { };

In general, thanks to the primary template, HasX<T, U> derives from std::false_type. However, there exists a specialization for U = int which derives from std::true_type. Therefore, HasX<bool, double>::value == false but HasX<bool, int>::value == true.

Thanks to the default for U, HasX<bool>::value == HasX<bool, int>::value == true.

4) decltype and a fancy way to say int:

A little digression here but, please, bear with me.

Basically (this is not entirely correct), decltype(expression) yields the type of expression. For instance, 0 has type int thus, decltype(0) means int. Analogously, 1.2 has type double and thus, decltype(1.2) means double.

Consider a function with this declaration:

char func(foo, int);

where foo is some class type. If f is an object of type foo, then decltype(func(f, 0)) means char (the type returned by func(f, 0)).

Now, the expression (1.2, 0) uses the (built-in) comma operator which evaluates the two sub-expressions in order (that is, first 1.2 and then 0), discards the first value and results in the second one. Hence,

int x = (1.2, 0);

is equivalent to

int x = 0;

Putting this together with decltype gives that decltype(1.2, 0) means int. There's nothing really special about 1.2 or double here. For instance, true has type bool and decltype(true, 0) means int as well.

What about a class type? For instace, what does decltype(f, 0) mean? It's natural to expect that this still means int but it might not be the case. Indeed, there might be an overload for the comma operator similar to the function func above that takes a foo and an int and returns a char. In this case, decltype(foo, 0) is char.

How can we avoid the use of a overload for the comma operator? Well, there's no way to overload the comma operator for a void operand and we can cast anything to void. Therefore, decltype((void) f, 0) means int. Indeed, (void) f casts f from foo to void which basically does nothing but saying that the expression must be considered as having type void. Then the built-in operator comma is used and ((void) f, 0) results in 0 which has type int. Hence, decltype((void) f, 0) means int.

Is this cast really necessary? Well, if there's no overload for the comma operator taking foo and int then this isn't necessary. We can always inspect the source code to see if there's such operator or not. However, if this appear in a template and f has type V which is a template parameter, then it's no longer clear (or even impossible to know) whether such overload for the comma operator exists or not. To be generic we cast anyway.

Bottom line: decltype((void) f, 0) is a fancy way to say int.

5) SFINAE:

This is a whole science ;-) OK I'm exagerating but it's not very simple either. So I'll keep the explanation to the bare minimum.

SFINAE stands for Substitution Failure is Not An Error. It means that when a template parameter is substituted by a type, an illegal C++ code might appear but, in some circunstances, instead of aborting compilation the compiler simply ignores the offending code as if it wasn't there. Let's see how it applies to our case:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

// Specialization for U = int
template <typename T>
struct HasX <T, decltype((void) T::x, 0)> : std::true_type { };

Here, again, decltype((void) T::x, 0) is a fancy way to say int but with the benefit of SFINAE.

When T is substituted with a type, an invalid construct might appear. For instance, bool::x is not valid C++, so substituting T with bool in T::x yields an invalid construct. Under the SFINAE principle, the compiler doesn't reject the code, it simply ignores (parts of) it. More precisely, as we have seenHasX<bool> means actually HasX<bool, int>. The specialization for U = int should be selected but, while instantiating it, the compiler finds bool::x and ignores the template specialization altogether as if it didn't exist.

At this point, the code is essencially the same as in case (2) above where just the primary template exists. Hence, HasX<bool, int>::value == false.

The same argument used for bool holds for B since B::x is an invalid construct (B has no member x). However, A::x is OK and the compiler sees no issue in instantiating the specialization for U = int (or, more precisely, for U = decltype((void) A::x, 0)). Hence, HasX<A>::value == true.

6) Unnaming U:

Well, looking at the code in (5) again, we see that the name U is not used anywhere but in its declaration (typename U). We can then unname the second template argument and we obtain the code shown at the top of this post.