Article
C++ reflection: move-constructible does not mean a move constructor exists
C++ reflection exposes whether special member functions are absent or deleted. A defaulted destructor and a type-trait example show why callability and declaration state answer different questions.
Share
Koharu's reading tip
A positive move trait can be satisfied by copying. Inspecting declarations adds the missing context when a class changes.

Adding = default to a destructor can change the move-related properties of a C++ class. That is one of the less obvious consequences of the special member function rules.
On October 26, 2026, ISO C++ Blog highlighted Lieven de Cock’s exploration of those rules through reflection, using Howard Hinnant’s table of declaration combinations.
The useful question is whether “constructible from an rvalue” means “has a move constructor.” Separating those ideas gives type traits and reflection distinct jobs.
An explicit destructor suppresses implicit move declarations
An implicitly declared move constructor requires, among other conditions, the absence of a user-declared destructor. Writing ~T() = default; still declares one. When no move constructor is supplied or implicitly declared, construction from an rvalue may use a copy constructor instead. The C++ working draft’s constructor rules explicitly describe this fallback.
This C++17 example demonstrates the distinction without reflection:
#include <type_traits>
// Purpose: model rvalue construction without a move constructor.
struct CopyFallback {
int value{};
// Explicit destructor. No arguments, return value, exceptions, or external effects.
~CopyFallback() = default;
};
static_assert(std::is_copy_constructible_v<CopyFallback>);
static_assert(std::is_move_constructible_v<CopyFallback>);
Both assertions hold. For a referenceable type, std::is_move_constructible_v<T> tests construction from T&&; it does not inspect whether a move constructor exists. A copy constructor accepting that argument can satisfy the type-trait definition.
The example was compile-checked with GCC 13.3.0 in C++17 mode. That verifies the trait results, not a C++26 reflection implementation.
members_of separates absent declarations from deleted functions
Reflection shifts the question from whether an expression works to which declarations exist. The C++26 reflection proposal P2996R13 provides ^^T to obtain a std::meta::info representing a type.
Calling std::meta::members_of(^^T, ctx) returns reflections of members accessible in the supplied context. Implicit special members are included, but so are other members such as data members. The vector size is therefore not a function count. The member-query specification defines the access and membership boundaries.
First select functions with is_function, then distinguish deleted functions with is_deleted. For narrower inspections, is_special_member_function selects special members and is_move_constructor identifies move constructors. These predicates belong to the reflection queries.
In de Cock’s worked examples, declaring a destructor removes implicit move operations from the list. User-declaring a move operation instead leaves implicit copy operations present but deleted. The copy-assignment rules also specify that latter relationship.
Absence and deletion are different observations. Keeping deleted functions in a separate count preserves the reason an operation is unavailable. Before treating a missing entry as an absent declaration, however, make sure the access context covers the members being compared.
The first declaration determines what defaulting means
Once a function is found, is_defaulted, is_user_declared, and is_user_provided describe different aspects of it.
For destructors, the explicit-defaulting rules produce the following classification. Each row describes a separate class T.
| Declaration pattern | user-declared | user-provided | defaulted |
|---|---|---|---|
| No written destructor declaration | false | false | true |
First declaration is ~T() = default; inside the class |
true | false | true |
~T(); inside, followed by T::~T() = default; outside |
true | true | true |
“User-provided” does not simply mean that someone wrote the function body. It means user-declared and not explicitly defaulted or deleted on the first declaration. Defaulting later can therefore make a function both user-provided and defaulted.
For implicit move declarations, the relevant destructor property is user-declared. Both explicit forms in the table qualify. Delegating the implementation to the compiler is different from leaving the declaration implicit.
Match the reflection environment to the question being tested
A C++26 language mode alone does not establish support for every required feature. The GCC support table, checked on October 27, 2026, lists P2996R13 reflection in GCC 16 with -freflection. The template for and define_static_array facilities used in de Cock’s enumeration code are listed separately.
The Clang support table, checked on the same date, lists P2996R13 as unsupported. Distinguish experimental forks from the regular compiler and select a version that implements the features the example actually uses.
The practical choice follows the question: use type traits to ask whether construction from particular arguments is valid, and reflection to inspect which special members are declared or deleted. Comparing both views during a class review helps explain changes that a callability check alone cannot reveal.
Source
- Title: C++ Reflection: Verifying Compiler-generated Functions -- Lieven de Cock
- URL: https://isocpp.org//blog/2026/10/cpp-reflection-verifying-compiler-generated-functions-lieven-de-cock
Share
Related Articles
These articles share nearby categories or tags, so you can keep reading along the same thread.




