20

I'm trying to understand the implementation of std::is_class. I've copied some possible implementations and compiled them, hoping to figure out how they work. That done, I find that all the computations are done during compilation (as I should have figured out sooner, looking back), so gdb can give me no more detail on what exactly is going on.

The implementation I'm struggling to understand is this one:

template<class T, T v>
    struct integral_constant{
    static constexpr T value = v;
    typedef T value_type;
    typedef integral_constant type;
    constexpr operator value_type() const noexcept {
        return value;
    }
};

namespace detail {
    template <class T> char test(int T::*);   //this line
    struct two{
        char c[2];
    };
    template <class T> two test(...);         //this line
}

//Not concerned about the is_union<T> implementation right now
template <class T>
struct is_class : std::integral_constant<bool, sizeof(detail::test<T>(0))==1 
                                                   && !std::is_union<T>::value> {};

I'm having trouble with the two commented lines. This first line:

 template<class T> char test(int T::*);

What does the T::* mean? Also, is this not a function declaration? It looks like one, yet this compiles without defining a function body.

The second line I want to understand is:

template<class T> two test(...);

Once again, is this not a function declaration with no body ever defined? Also what does the ellipsis mean in this context? I thought an ellipsis as a function argument required one defined argument before the ...?

I would like to understand what this code is doing. I know I can just use the already implemented functions from the standard library, but I want to understand how they work.

References:

Toby Speight
  • 27,591
  • 48
  • 66
  • 103

6 Answers6

17

What you are looking at is some programming technologie called "SFINAE" which stands for "Substitution failure is not an error". The basic idea is this:

namespace detail {
  template <class T> char test(int T::*);   //this line
  struct two{
    char c[2];
  };
  template <class T> two test(...);         //this line
}

This namespace provides 2 overloads for test(). Both are templates, resolved at compile time. The first one takes a int T::* as argument. It is called a Member-Pointer and is a pointer to an int, but to an int thats a member of the class T. This is only a valid expression, if T is a class. The second one is taking any number of arguments, which is valid in any case.

So how is it used?

sizeof(detail::test<T>(0))==1

Ok, we pass the function a 0 - this can be a pointer and especially a member-pointer - no information gained which overload to use from this. So if T is a class, then we could use both the T::* and the ... overload here - and since the T::* overload is the more specific one here, it is used. But if T is not a class, then we cant have something like T::* and the overload is ill-formed. But its a failure that happened during template-parameter substitution. And since "substitution failures are not an error" the compiler will silently ignore this overload.

Afterwards is the sizeof() applied. Noticed the different return types? So depending on T the compiler chooses the right overload and therefore the right return type, resulting in a size of either sizeof(char) or sizeof(char[2]).

And finally, since we only use the size of this function and never actually call it, we dont need an implementation.

Anedar
  • 4,235
  • 1
  • 23
  • 41
  • What do you mean by "more specific"? – curiousguy Jun 23 '18 at 23:01
  • 2
    @curiousguy: The `T::*` overload can only be used for expressions that are convertible to a member-pointer. The `...` overload could be used for any expression. – Anedar Jun 24 '18 at 13:43
  • Is that a standard term? – curiousguy Jun 24 '18 at 17:53
  • I think instead of "overload", "specialization" is more precise word. For function templates, the type parameters are deduced from the parameters. In your particular example, the choices are between test(int T::*) and test(...). – moyang_mm Nov 15 '20 at 05:19
15

Part of what is confusing you, which isn't explained by the other answers so far, is that the test functions are never actually called. The fact they have no definitions doesn't matter if you don't call them. As you realised, the whole thing happens at compile time, without running any code.

The expression sizeof(detail::test<T>(0)) uses the sizeof operator on a function call expression. The operand of sizeof is an unevaluated context, which means that the compiler doesn't actually execute that code (i.e. evaluate it to determine the result). It isn't necessary to call that function in order to know the sizeof what the result would be if you called it. To know the size of the result the compiler only needs to see the declarations of the various test functions (to know their return types) and then to perform overload resolution to see which one would be called, and so to find what the sizeof the result would be.

The rest of the puzzle is that the unevaluated function call detail::test<T>(0) determines whether T can be used to form a pointer-to-member type int T::*, which is only possible if T is a class type (because non-classes can't have members, and so can't have pointers to their members). If T is a class then the first test overload can be called, otherwise the second overload gets called. The second overload uses a printf-style ... parameter list, meaning it accepts anything, but is also considered a worse match than any other viable function (otherwise functions using ... would be too "greedy" and get called all the time, even if there's a more specific function t hat matches the arguments exactly). In this code the ... function is a fallback for "if nothing else matches, call this function", so if T isn't a class type the fallback is used.

It doesn't matter if the class type really has a member variable of type int, it is valid to form the type int T::* anyway for any class (you just couldn't make that pointer-to-member refer to any member if the type doesn't have an int member).

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 5
    plus 1 for the last three lines. – RaGa__M Jun 14 '17 at 09:54
  • Why not use `char T::*`? I still don't understand why `int` is used here. – Lin Jul 06 '23 at 13:39
  • @Lin, why do you think that would be any different? The class doesn't have a `char` member either. Read my answer again, I already explained it. All that matters is whether `int T::*` is a valid type to talk about, which is true for any class type, even if it doesn't have an `int` member. You could use `char T::*` or `float T::*` or `std::string T::*` ... it doesn't matter. Since it doesn't matter, using `int` is fine. Why _not_ use `int` here? – Jonathan Wakely Jul 06 '23 at 14:56
  • @JonathanWakely, If a class, `T`, doesn't even have a member of type `int`, then how does the compiler resolve `int T::*` and pick the first `test` definition? For instance, in terms of an empty struct, how can that work? – Lin Jul 10 '23 at 19:22
  • @Lin, **read the answer!** The last paragraph already covers that. The compiler doesn't need to "resolve" anything, the _type_ `int T::*` is valid even if there is no member that it could ever point to. Just like the type `void(*)(int, int, double)` is a valid pointer-to-function type even if your program does not have any functions with that signature. – Jonathan Wakely Jul 11 '23 at 12:17
  • @JonathanWakely, Thanks! I think now I understand! `int T::*` is just a type! In the context of `test`, it doesn't really matter whether we can construct a value with such type. – Lin Jul 11 '23 at 19:41
4

The std::is_class type trait is expressed through a compiler intrinsic (called __is_class on most popular compilers), and it cannot be implemented in "normal" C++.

Those manual C++ implementations of std::is_class can be used in educational purposes, but not in a real production code. Otherwise bad things might happen with forward-declared types (for which std::is_class should work correctly as well).

Here's an example that can be reproduced on any msvc x64 compiler.

Suppose I have written my own implementation of is_class:

namespace detail
{
    template<typename T>
    constexpr char test_my_bad_is_class_call(int T::*) { return {}; }

    struct two { char _[2]; };

    template<typename T>
    constexpr two test_my_bad_is_class_call(...) { return {}; }
}

template<typename T>
struct my_bad_is_class
    : std::bool_constant<sizeof(detail::test_my_bad_is_class_call<T>(nullptr)) == 1>
{
};

Let's try it:

class Test
{
};

static_assert(my_bad_is_class<Test>::value == true);
static_assert(my_bad_is_class<const Test>::value == true);

static_assert(my_bad_is_class<Test&>::value == false);
static_assert(my_bad_is_class<Test*>::value == false);
static_assert(my_bad_is_class<int>::value == false);
static_assert(my_bad_is_class<void>::value == false);

As long as the type T is fully defined by the moment my_bad_is_class is applied to it for the first time, everything will be okay. And the size of its member function pointer will remain what it should be:

// 8 is the default for such simple classes on msvc x64
static_assert(sizeof(void(Test::*)()) == 8);

However, things become quite "interesting" if we use our custom type trait with a forward-declared (and not yet defined) type:

class ProblemTest;

The following line implicitly requests the type int ProblemTest::* for a forward-declared class, definition of which cannot be seen by the compiler right now.

static_assert(my_bad_is_class<ProblemTest>::value == true);

This compiles, but, unexpectedly, breaks the size of a member function pointer.

It seems like the compiler attempts to "instantiate" (similarly to how templates are instantiated) the size of a pointer to ProblemTest's member function in the same moment that we request the type int ProblemTest::* within our my_bad_is_class implementation. And, currently, the compiler cannot know what it should be, thus it has no choice but to assume the largest possible size.

class ProblemTest // definition
{
};

// 24 BYTES INSTEAD OF 8, CARL!
static_assert(sizeof(void(ProblemTest::*)()) == 24);

The size of a member function pointer was trippled! And it cannot be shrunk back even after the definition of class ProblemTest has been seen by the compiler.

If you work with some third party libraries that rely on particular sizes of member function pointers on your compiler (e.g., the famous FastDelegate by Don Clugston), such unexpected size changes caused by some call to a type trait might be a real pain. Primarily because type trait invocations are not supposed to modify anything, yet, in this particular case, they do -- and this is extremely unexpected even for an experienced developer.

On the other hand, had we implemented our is_class using the __is_class intrinsic, everything would have been OK:

template<typename T>
struct my_good_is_class
    : std::bool_constant<__is_class(T)>
{
};

class ProblemTest;

static_assert(my_good_is_class<ProblemTest>::value == true);

class ProblemTest
{
};

static_assert(sizeof(void(ProblemTest::*)()) == 8);

Invocation of my_good_is_class<ProblemTest> does not break any sizes in this case.

So, my advice is to rely on the compiler intrinsics when implementing your custom type traits like is_class wherever possible. That is, if you have a good reason to implement such type traits manually at all.

Taras
  • 488
  • 3
  • 15
  • 2
    That's really some interesting find. That suggests we should use void_t idiom (instead of function overload) wherever possible for detection using member pointer. See this godbolt example: https://godbolt.org/z/7M8qo3 , you can change the BAD_IS_CLASS_TYPE macro value to check different behaviors. – YumeYao Sep 17 '20 at 03:07
  • 1
    So it seems that when MSVC instantiates the calling convention of detail::test_call(int T::*), some extra information on T is instantiated as well - that includes T's member function pointer type and its size – YumeYao Sep 17 '20 at 03:18
3

What does the T::* mean? Also, is this not a function declaration? It looks like one, yet this compiles without defining a function body.

The int T::* is a pointer to member object. It can be used as follows:

struct T { int x; }
int main() {
    int T::* ptr = &T::x;

    T a {123};
    a.*ptr = 0;
}

Once again, is this not a function declaration with no body ever defined? Also what does the ellipsis mean in this context?

In the other line:

template<class T> two test(...);

the ellipsis is a C construct to define that a function takes any number of arguments.

I would like to understand what this code is doing.

Basically it's checking if a specific type is a struct or a class by checking if 0 can be interpreted as a member pointer (in which case T is a class type).

Specifically, in this code:

namespace detail {
    template <class T> char test(int T::*);
    struct two{
        char c[2];
    };
    template <class T> two test(...);
}

you have two overloads:

  • one that is matched only when a T is a class type (in which case this one is the best match and "wins" over the second one)
  • on that is matched every time

In the first the sizeof the result yields 1 (the return type of the function is char), the other yields 2 (a struct containing 2 chars).

The boolean value checked is then:

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value

which means: return true only if the integral constant 0 can be interpreted as a pointer to member of type T (in which case it's a class type), but it's not a union (which is also a possible class type).

Community
  • 1
  • 1
Shoe
  • 74,840
  • 36
  • 166
  • 272
2

Test is an overloaded function that either takes a pointer to member in T or anything. C++ requires that the best match be used. So if T is a class type it can have a member in it...then that version is selected and the size of its return is 1. If T is not a class type then T::* make zero sense so that version of the function is filtered out by SFINAE and won't be there. The anything version is used and it's return type size is not 1. Thus checking the size of the return of calling that function results in a decision whether the type might have members...only thing left is making sure it's not a union to decide if it's a class or not.

Edward Strange
  • 40,307
  • 7
  • 73
  • 125
2

Here is standard wording:

[expr.sizeof]:

The sizeof operator yields the number of bytes occupied by a non-potentially-overlapping object of the type of its operand.

The operand is either an expression, which is an unevaluated operand ([expr.prop])......

2. [expr.prop]:

In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp]).

An unevaluated operand is not evaluated.

3. [temp.fct.spec]:

  1. [Note: Type deduction may fail for the following reasons:

...

(11.7) Attempting to create “pointer to member of T” when T is not a class type. [ Example:

  template <class T> int f(int T::*);
  int i = f<int>(0);

— end example ]

As above shows, it is well-defined in standard :-)

4. [dcl.meaning]:

[Example:

struct X {
void f(int);
int a;
};
struct Y;

int X::* pmi = &X::a;
void (X::* pmf)(int) = &X::f;
double X::* pmd;
char Y::* pmc;

declares pmi, pmf, pmd and pmc to be a pointer to a member of X of type int, a pointer to a member of X of type void(int), a pointer to a member ofX of type double and a pointer to a member of Y of type char respectively.The declaration of pmd is well-formed even though X has no members of type double. Similarly, the declaration of pmc is well-formed even though Y is an incomplete type.

Chen Li
  • 4,824
  • 3
  • 28
  • 55