The compiler generates special member functions, according to different rules. These have been described, and have been summarized in a nice table, known as “Howard Hinnants’ table” (https://howardhinnant.github.io/smf.jpg, https://howardhinnant.github.io/classdecl.html).
By using reflection we can verify, if this is indeed correct (which it is), as such we will refresh our knowledge of the table, and see some reflection mechanisms at work.
We will use several toy examples, each creating a struct called TestX, where X is the number of the row in the above mentioned table.
The struct also contains 2 integer member variables, and another method doSomething(), as for our research of the table, they don’t matter, but they do matter for the reflection mechanisms to point out some key differences.
Used reflection mechanics
This is our starting toy structure:

The function analyzing every type is templatized based upon the type to inspect, and goes like this, we will walk through it step by step.

Basics applied:
- reflect on the structure/type, by using the lift/reflection operator: ^^T, which gives us a std::meta_info value, which we store for later use.
- next we ask for all the members of the struct/type (members_of), and ask for the count of those. For the access_context, see my first article in the series.
- we print out this count
- we use identifier_of, to also print out the name of the type/struct we are reflecting on
- next we loop over all those members (using the ‘template for‘ construct) which we asked for a second time and we stored those in a static array
- there is NO reflection method which gives you just the member methods (there is one to just get the data members though)
- which means during the loop, we need to check if a certain member is actually a function (std::meta::is_function(xxx))
- if it is, it is of interest to us
- however a method could be deleted, which means it will pop up in this looping, but we should consider it as excluded: is_deleted(member)
- we ask for its display string (not everything has an identifier) by means of display_string_of(member)
- and in case it would have an identifier (spoiler alert, as for the methods, only our doSomething method has one) we print that one out too ((has_identifier(member), identifier_of(member))
- and we increment the counter of methods spotted, which we print out after the loop
- the ‘full details’ part is covered later on in the “We want even more” section
- we also count the deleted functions
As such, we print out all the methods encountered and we can inspect that outcome to see if it matches what the table tells us (always deduct the number of methods by 1, to exclude our doSomething from the equation)
That’s our mechanics, our little tool, time to start applying it to our different structs. Buckle up, here we go.
Note, that user declaration is sufficient to get the mechanisms rolling, we are not even touching the realm of their implementation.
Row1

This gives the following output:
Number of members for Test1 : 9
void {anonymous}::Test1::doSomething()
with identifier doSomething
constexpr {anonymous}::Test1::Test1()
constexpr {anonymous}::Test1::Test1(const {anonymous}::Test1&)
constexpr {anonymous}::Test1& {anonymous}::Test1::operator=(const {anonymous}::Test1&)
constexpr {anonymous}::Test1::Test1({anonymous}::Test1&&)
constexpr {anonymous}::Test1& {anonymous}::Test1::operator=({anonymous}::Test1&&)
constexpr {anonymous}::Test1::~Test1()
Number of methods for Test1 : 7
Number of deleted methods for Test1 : 0Conclusions:
- default constructor
- copy constructor
- copy assignment operator
- move constructor
- move assignment operator
- destructor
We got 6/6.
Row 2
We declare a custom constructor, which means we loose the default constructor.

Output:
Number of members for Test2 : 9
{anonymous}::Test2::Test2(int)
void {anonymous}::Test2::doSomething()
with identifier doSomething
constexpr {anonymous}::Test2::Test2(const {anonymous}::Test2&)
constexpr {anonymous}::Test2& {anonymous}::Test2::operator=(const {anonymous}::Test2&)
constexpr {anonymous}::Test2::Test2({anonymous}::Test2&&)
constexpr {anonymous}::Test2& {anonymous}::Test2::operator=({anonymous}::Test2&&)
constexpr {anonymous}::Test2::~Test2()
Number of methods for Test2 : 7
Number of deleted methods for Test2 : 0Conclusions:
- the default constructor is indeed gone
- the other 5 are still there
We got 5/6, and added a custom constructor ==> 5 + 1 = 6 methods
Row 3
We declare the default constructor ourselves.

Output:
Number of members for Test3 : 9
{anonymous}::Test3::Test3()
void {anonymous}::Test3::doSomething()
with identifier doSomething
constexpr {anonymous}::Test3::Test3(const {anonymous}::Test3&)
constexpr {anonymous}::Test3& {anonymous}::Test3::operator=(const {anonymous}::Test3&)
constexpr {anonymous}::Test3::Test3({anonymous}::Test3&&)
constexpr {anonymous}::Test3& {anonymous}::Test3::operator=({anonymous}::Test3&&)
constexpr {anonymous}::Test3::~Test3()
Number of methods for Test3 : 7
Number of deleted methods for Test3 : 0Conclusions:
- as in row 1
We get 6/6.
Row 4
This is an example of a mistake often made, that mistake typically appears in one of the following forms (remember declaration is sufficient for the repercussions):
- declare a destructor with an empty implementation “{}”
- declare a destructor and “= default”

Output:
Number of members for Test4 : 7
constexpr {anonymous}::Test4::~Test4()
void {anonymous}::Test4::doSomething()
with identifier doSomething
constexpr {anonymous}::Test4::Test4()
constexpr {anonymous}::Test4::Test4(const {anonymous}::Test4&)
constexpr {anonymous}::Test4& {anonymous}::Test4::operator=(const {anonymous}::Test4&)
Number of methods for Test4 : 5
Number of deleted methods for Test4 : 0Conclusions:
- no more move constructor
- no more move assignment operator
- both are not declared, and as such not in the list of members
We get 4/6, we have lost “move”.
Row 5
We have user declared the copy constructor.

Output:
Number of members for Test5 : 6
{anonymous}::Test5::Test5(const {anonymous}::Test5&)
void {anonymous}::Test5::doSomething()
with identifier doSomething
constexpr {anonymous}::Test5& {anonymous}::Test5::operator=(const {anonymous}::Test5&)
constexpr {anonymous}::Test5::~Test5()
Number of methods for Test5 : 4
Number of deleted methods for Test5 : 0Conclusions:
- we lost the default constructor (remember the moment we declare any other constructor we loose it)
- we again lost the 2 ‘move’ methods
- both are not declared, and as such not in the list of members
We got 3/6.
Row 6
We have user declared the copy assignment operator.

Output:
Number of members for Test6 : 7
{anonymous}::Test6& {anonymous}::Test6::operator=(const {anonymous}::Test6&)
void {anonymous}::Test6::doSomething()
with identifier doSomething
constexpr {anonymous}::Test6::Test6()
constexpr {anonymous}::Test6::Test6(const {anonymous}::Test6&)
constexpr {anonymous}::Test6::~Test6()
Number of methods for Test6 : 5
Number of deleted methods for Test6 : 0Conclusions:
- we again lost the 2 ‘move’ methods
- both are not declared, and as such not in the list of members
We got 4/6.
Row 7
We have user declared the move constructor.

Output:
Number of members for Test7 : 7
{anonymous}::Test7::Test7(const {anonymous}::Test7&&)
void {anonymous}::Test7::doSomething()
with identifier doSomething
constexpr {anonymous}::Test7::~Test7()
Number of methods for Test7 : 3
Number of deleted methods for Test7 : 2Conclusions:
- we lost the default constructor (remember the moment we declare any other constructor we loose it)
- we lost the 2 ‘copy’ methods, and this time they are “deleted“
- we also loose the move assignment operator (not declared and as such not in the list of members)
We got 2/6.
Row 8
We have user declared the move assignment operator.

Output:
Number of members for Test8 : 8
{anonymous}::Test8& {anonymous}::Test8::operator=(const {anonymous}::Test8&&)
void {anonymous}::Test8::doSomething()
with identifier doSomething
constexpr {anonymous}::Test8::Test8()
constexpr {anonymous}::Test8::~Test8()
Number of methods for Test8 : 4
Number of deleted methods for Test8 : 2Conclusions:
- we again lost the 2 ‘copy’ methods (deleted)
- we also loose the move constructor (not declared and as such not in the list of members)
We got 3/6.
Conclusion
The table is correctly verified by using reflection. Sometimes things are according to expectations 🙂
Only in the Row7/8 case when we loose the “copy” methods, they are “deleted”, in all other cases when we lost a method it was with the status “not declared”
In all cases, the reflection showed us:
- destructor is always there
- when we loose a copy method, it is in the list, with status ‘deleted’
- when we loose a move method, it is ‘not declared’, meaning it is not in the members list
- when we loose the default constructor, it is not present in the members list.
Could we add some bonus inspections ? Can we think of other methods the compiler generates for us …
Spaceship operator: <=>
Time to place your bets, say we default declare it (that’s the use case we would like to have), how many new methods would we get:
- 1 (<=>)
- 2 (<=> and ==)
- 6 (<, >, <=, >=, ==, !=)
- something else
Let’s use the following toy example:

And the output is:
Number of members for SpaceShip : 11
constexpr auto {anonymous}::SpaceShip::operator<=>(const {anonymous}::SpaceShip&) const
void {anonymous}::SpaceShip::doSomething()
with identifier doSomething
constexpr {anonymous}::SpaceShip::SpaceShip()
constexpr {anonymous}::SpaceShip::SpaceShip(const {anonymous}::SpaceShip&)
constexpr {anonymous}::SpaceShip& {anonymous}::SpaceShip::operator=(const {anonymous}::SpaceShip&)
constexpr {anonymous}::SpaceShip::SpaceShip({anonymous}::SpaceShip&&)
constexpr {anonymous}::SpaceShip& {anonymous}::SpaceShip::operator=({anonymous}::SpaceShip&&)
constexpr {anonymous}::SpaceShip::~SpaceShip()
constexpr bool {anonymous}::SpaceShip::operator==(const {anonymous}::SpaceShip&) const
Number of methods for SpaceShip : 9
Number of deleted methods for SpaceShip : 0Conclusion:
- we get 2 extra methods
- operator<=>
- operator==
The correct answer was “2”. Did you predict it correctly ?
We want more, we want more: DesDeMovA
Ok, let’s do it. Many people if they have a class, and they don’t want it to be copied nor moved do the following.
Both are correct, but much more code than actually needed is written.

Giving the output:
Number of members for OverDisable1 : 6
void {anonymous}::OverDisable1::doSomething()
with identifier doSomething
constexpr {anonymous}::OverDisable1::~OverDisable1()
Number of methods for OverDisable1 : 2
Number of deleted methods for OverDisable1 : 2
Number of members for OverDisable2 : 8
void {anonymous}::OverDisable2::doSomething()
with identifier doSomething
constexpr {anonymous}::OverDisable2::~OverDisable2()
Number of methods for OverDisable2 : 2
Number of deleted methods for OverDisable2 : 4In comes the rule “DesDeMovA” coined by Peter Sommerlad: https://safecpp.com/2019/07/01/initial.html.
It is sufficient to only delete the move assignment operator. Because:
- we loose the move constructor (-1)
- we loose the copy operations (- 2)
- we deleted explicitly the move assignment operator (-1)
6 – 1 – 2 – 1 = 2 ==> constructor and destructor.
and our inspected output is:
Number of members for DesDeMovA : 8
void {anonymous}::DesDeMovA::doSomething()
with identifier doSomething
constexpr {anonymous}::DesDeMovA::DesDeMovA()
constexpr {anonymous}::DesDeMovA::~DesDeMovA()
Number of methods for DesDeMovA : 3We want even more
Let’s use some more inspection methods:
- is_defaulted
- is_user_declared
- is_user_provided
What does this all mean : is_defaulted
We have 2 scenarios to end up in is_defaulted
- we didn’t mention the special method at all (like in many of our scenarios)
- we did mention the method and said “= default”
What does this all mean : is_user_declared
This is true the moment we declare the method ourselves (again irrelevant of the implementation)
What does this all mean : is_user_provided
This requires it is user_declared, since we first need to declare before we can provide it. Note, again this does not look at the implementation (like in our Row examples we did not provide implementations (mostly)).
So what’s the difference than between is_user_declared and is_user_provided, something is user declared, but not user provided if that declaration (or definition in cpp file) mentioned “= default”.
So basically when it is user declared, next to that it is EITHER is_defaulted or is_user_provided.
Inspecting the entire output for our 8 rows, is considered homework for the reader.
But let’s just pick one, say Row 3, here’s the output of it with full details (shortened to the constructor only).
printMethods<Test3>(true);
Number of members for Test3 : 9
{anonymous}::Test3::Test3()
is_defaulted: false
is_user_provided: true
is_user_declared: trueLook at the constructor:
- it is user declared
- it is user provided
- it is NOT defaulted
Let’s adjust the struct to Test3Bis as follows, we declare the constructor, but default it:

Which get the following output ((shortened to the constructor only)):
printMethods<Test3Bis>(true);
Number of members for Test3Bis : 9
constexpr {anonymous}::Test3Bis::Test3Bis()
is_defaulted: true
is_user_provided: false
is_user_declared: trueAgain let’s look at the constructor:
- it is user declared
- it is NOT user provided
- it is defaulted
Time to close up our little journey of experimenting with reflection, what did we learn (for some things see the first article for more information):
- reflection/lift operator (^^)
- members_of
- access_context (current())
- define_static_array
- identifier_of
- has_identifier
- display_string_of
- is_function
- is_deleted
- is_defaulted
- is_user_declared
- is_user_provided
You can play with the code online here: https://godbolt.org/z/daeWhed1b
Reference to the first article: https://www.linkedin.com/pulse/c-reflection-universal-printer-lieven-de-cock-abzze

Leave a Reply