C++ Reflection: Verifying Compiler-generated Functions

C++ Reflection: Verifying Compiler-generated Functions

By Lieven de Cock

Overload, 34(194):9-13, June 2026


The compiler generates special member functions as shown in Hinnant’s Table. Lieven de Cock shows how reflection can be used to verify them.

The compiler generates special member functions, according to specific rules. These have been described, and have been summarized in a nice table, known as Howard Hinnants’ table [Hinnant20].

Using reflection, we can verify whether this is indeed correct (which it is), 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 contains two integer member variables and a method (doSomething()). What these do doesn’t matter, but they are needed for the reflection mechanism to be able to point out some key differences.

Used reflection mechanics

This is our starting toy structure, which is the test for row 1 in the table:

  struct Test1
  {
    int x{};
    int y{};
    
    void doSomething() {};
  };

The function analyzing every type is templatized based upon the type to inspect (see Listing 1). We will walk through it step by step.

template <typename T>
void printMethods(bool fullDetails = false)
{
  constexpr auto ctx 
    = std::meta::access_context::current();
  constexpr auto refl{^^T};
  constexpr auto numberMembers 
    = members_of(refl, ctx).size();

  std::println("Number of members for {} : {}",
    identifier_of(refl), numberMembers);

  int methods{};
  int deleted{};
  template for (constexpr auto member :
      // gives shadow warning :-(
      define_static_array(members_of(^^T, ctx)))
    {
    if constexpr (std::meta::is_function(member)
                  && !is_deleted(member))
    {
      constexpr std::string_view name 
        = display_string_of(member);
      std::println("  {}", name);
      if constexpr (has_identifier(member))
      {
        std::println("     with identifier {}",
                     identifier_of(member));
      }
      ++methods;

      // do some inspecting
      if(fullDetails)
      {
        std::println("     is_defaulted: {}",
                     is_defaulted(member));
        std::println("     is_user_provided: {}",
                     is_user_provided(member));
        std::println("     is_user_declared: {}", 
                     is_user_declared(member));
      }
    }
    // extra: let'salso count deleted functions
    if constexpr (std::meta::is_function(member)
      && is_deleted(member))
    {
      ++deleted;
    }
  }
  std::println("Number of methods for {} : {}",
               identifier_of(refl), methods);
  std::println("Number of deleted methods for {}"
         " : {}", identifier_of(refl), deleted);
  std::println();
Listing 1

Basics applied:

  • reflect on the structure/type, using the lift/reflection operator: ^^T, which gives us a std::meta_info value that 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 [deCock26].
  • 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 in the ‘We want more, we want more: DesDeMovA’ 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

  struct Test1
  {
    int x{};
    int y{};
    
    void doSomething() {};
  };

This gives the output in Figure 1.

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 : 0
Figure 1

Conclusions:

  • 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 lose the default constructor.

  struct Test2
  {
    int x{};
    int y{};
  
    Test2(int); // just declared is enough
    void doSomething() {};
  };

The output is in Figure 2.

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 : 0
Figure 2

Conclusions:

  • 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.

  struct Test3
  {
    int x{};
    int y{};
  
    Test3();     // just declared is enough
    void doSomething() {};
  };

The output is in Figure 3.

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 : 0
Figure 3

Conclusions as for Row 1.

We get 6/6.

Row 4

This is an example of a mistake often made, which 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)
  struct Test4
  {
    int x{};
    int y{};
    
    ~Test4() = default; // just declared is enough, 
    // but here is the typical mistake of 
    // setting it to default - not needed at all
    void doSomething() {};
  };

The output is in Figure 4.

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 : 0
Figure 4

Conclusions:

  • 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.

  struct Test5
  {
    int x{};
    int y{};
    
    Test5(const Test5&); // just declared is enough
    void doSomething() {};
  };

The output is in Figure 5.

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 : 0
Figure 5

Conclusions:

  • we lost the default constructor (remember the moment we declare any other constructor, we lose 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.

  struct Test6
  {
    int x{};
    int y{};
    Test6& operator=(const Test6&); // just
                      // declared is enough
    void doSomething() {};
  };

The output is in Figure 6.

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 : 0
Figure 6

Conclusions:

  • 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 copy constructor.

  struct Test7
  {
    int x{};
    int y{};
    Test7(const Test7&&);  // just declared is 
                           // enough
    void doSomething() {};
  };

The output is in Figure 7.

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 : 2
Figure 7

Conclusions:

  • we lost the default constructor (remember: the moment we declare any other constructor, we lose it)
  • we lost the 2 copy methods, and this time they are ‘deleted’
  • we also lose 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.

  struct Test8
  {
    int x{};
    int y{};
    
    Test8& operator=(const Test8&&); // just 
                       // declared is enough
    void doSomething() {};
  };

The output is in Figure 8.

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 : 2
Figure 8

Conclusions:

  • we again lost the 2 copy methods (deleted)
  • we also lose 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 cases of Row 7 and Row 8 are the lost copy methods ‘deleted’. In all other cases, when a method was lost it was with a status of ‘not declared’.

In all cases, the reflection showed us:

  • destructor is always there
  • when we lose a copy method, it is in the list, with status ‘deleted’
  • when we lose a move method, it is ‘not declared’, meaning it is not in the members list
  • when we lose 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:

  struct SpaceShip
  {
    int x{};
    int y{};
    auto operator<=>(const SpaceShip&) 
      const = default;
    void doSomething() {};
  };

And the output is in Figure 9.

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 : 0
Figure 9

Conclusion:

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 or moved, do the following.

  struct OverDisable1
  {
    int x{};
    int y{};
    
    void doSomething() {};
    
    OverDisable1(const OverDisable1&) = delete;
    OverDisable1& operator=(const OverDisable1&) 
      = delete;
  };
  struct OverDisable2
  {
    int x{};
    int y{};
    void doSomething() {};
    OverDisable2(const OverDisable2&) = delete;
    OverDisable2& operator=(const OverDisable2&) 
      = delete;
    OverDisable2(const OverDisable2&&) = delete;
    OverDisable2& operator=(const OverDisable2&&) 
      = delete;
  };

Both are correct, but much more code than actually needed is written, giving the output in Figure 10.

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 : 4
Figure 10

In comes the rule DesDeMovA, coined by Peter Sommerlad [Sommerlad19]. It is sufficient to only delete the move assignment operator, because:

  • we lose the move constructor (-1)
  • we lose the copy operations (-2)
  • we explicitly deleted the move assignment operator (-1)

6 - 1 - 2 - 1 = 2 ⇒ constructor and destructor.

Our inspected output is in Figure 11.

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 : 3
Figure 11

We 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: true

Look 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:

  struct Test3Bis
  {
    int x{};
    int y{};
    
    Test3Bis() = default;
    void doSomething() {};
  };

which gets the output in Figure 12 (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: true
Figure 12

Again 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

References

[deCock26] Lieven de Cock, ‘C++ Reflection: a Universal Printer’, posted on 5 May 2026 and available at https://www.linkedin.com/pulse/c-reflection-universal-printer-lieven-de-cock-abzze

[Hinnant20] Howard Hinnant, ‘How I Declare My class and Why’, posted 24 February 2020 on Github and available at
https://howardhinnant.github.io/classdecl.html. (The table image is https://howardhinnant.github.io/smf.jpg.)

[Sommerlad19] Peter Sommerlad, ‘Introducing the Rule of DesDeMovA’, posted 1 July 2019 on Safe C++ Blog, available at https://safecpp.com/2019/07/01/initial.html

Lieven de Cock Lieven is a software developer, architect, team lead, manager, coach and mentor, with 30 years of experience. He is passionate about C++, software craftsmanship, and clean code. Recently he founded his own consulting company CppDriven, providing services in coaching and workshops for teams on modern C++ and its eco-system of tools.

This article was previously published on LinkedIn on 2 July 2026 at https://www.linkedin.com/pulse/draft/preview/7477750928557670400/






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.