C++ Module 18 – C++26 previews: reflection, contracts and safety
C++26 annotations: generating code from structs
Attach constant values to members with C++26 annotations, read them back with reflection, and generate CSV writers and option parsers from them.
What you will learn
- Annotate declarations with constant values using the [[=value]] syntax
- Read annotations back with annotations_of_with_type and extract
- Generate a CSV writer and a command-line option parser from annotated structs
- Measure what reflection-generated code costs at compile time
Before you start
What you’ll learn · Before you start
What you will learn
- Annotate declarations with constant values using the [[=value]] syntax
- Read annotations back with annotations_of_with_type and extract
- Generate a CSV writer and a command-line option parser from annotated structs
- Measure what reflection-generated code costs at compile time
Before you start
On this page
A CSV export of an order line with four columns needs four column names, and they usually sit in a list beside the
struct that goes stale when someone renames a member. An annotation attaches a constant value to a declaration,
written [[=value]], and C++26 reflection can read that value back while the program compiles. That turns a struct
into a description of what generic code should do with it: [[=Column("order_id")]] int id; tells a CSV writer the
column’s name, [[=Option("--port", "port to listen on")]] int port; tells an option parser the flag. The generic
code reads the annotations with std::meta::annotations_of_with_type and std::meta::extract, then generates the
header, the rows or the parsing loop, so no second list of names has to be kept in step with the struct. One rule
shapes everything else: an annotation’s value must be a constant of a
structural type: A type that may be the type of a non-type template parameter: a scalar type such as int, double or a pointer, an lvalue reference, or (C++20) a literal class whose bases and non-static data members are all public, not mutable and structural themselves. More in “Non-type and template template parameters”, which rules out members such as std::string_view.
Preview: GCC 16 only
Status: in the final C++26 draft. Annotations and reflection compiled only with GCC 16.2 and
-std=c++26 -freflection in these runs; Apple clang 21 supports neither.
An annotation is a value attached to a declaration
Annotations look like attributes, but they do a different job. An attribute such as [[nodiscard]] is a message to
the compiler, and code cannot read it. An annotation is a value for your own code: the expression after = is
evaluated while compiling, and its result stays attached to the declaration for reflection to find. A declaration
may carry several annotations, in one pair of brackets or several, and annotations may be applied to types,
variables, functions and their parameters, namespaces, enumerators, data members and base classes. On an enumerator
the annotation follows the name, as in low [[=Label("Low")]].
Side by side, the two look alike but do different jobs:
| Question | An attribute: [[nodiscard]] |
An annotation: [[=Column("qty")]] |
|---|---|---|
| Who reads it | The compiler | Your own code, through reflection |
| What it holds | A request, such as a warning | A constant value |
| How code reads it | It cannot | With annotations_of and extract |
The value must be a constant that could also be a template argument, which is what “structural type” means: a
scalar such as an int or a pointer, or a class whose data members are all public and themselves structural.
std::string_view fails that test, because its members are private, and a pointer to a string literal is not allowed
as such a constant either. The usual answer is an annotation class whose consteval constructor copies the text with
std::define_static_string and keeps a const char* to the copy, which is a valid constant.
Checkpoint
Check: An annotation is a value
Reflection reads annotations back while compiling
// Column names attached to members with annotations, and a CSV header generated from them.
#include <cstddef>
#include <meta>
#include <print>
#include <string>
#include <string_view>
#include <vector>
// An annotation type. Its values must be constants of the program, so it holds a pointer to a string with static
// storage, which std::define_static_string makes while compiling.
struct Column {
const char* name;
consteval Column(std::string_view text) : name(std::define_static_string(text)) {}
};
struct OrderLine {
[[=Column("order_id")]] int id;
[[=Column("item")]] std::string item;
[[=Column("qty")]] int quantity;
[[=Column("amount_paise")]] long long amount_paise;
int retries; // no annotation, so not a column
};
// The column name of a member, or nullptr when it has no Column annotation.
consteval const char* column_of(std::meta::info member) {
std::vector<std::meta::info> notes = std::meta::annotations_of_with_type(member, ^^Column);
return notes.empty() ? nullptr : std::meta::extract<Column>(notes[0]).name;
}
template <typename T>
std::string csv_header() {
std::string line;
template for (constexpr std::meta::info m :
std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
constexpr const char* name = column_of(m);
if constexpr (name != nullptr) {
line += line.empty() ? "" : ",";
line += name;
}
}
return line;
}
int main() {
std::println("{}", csv_header<OrderLine>());
template for (constexpr std::meta::info m :
std::define_static_array(std::meta::nonstatic_data_members_of(^^OrderLine, std::meta::access_context::current()))) {
constexpr std::size_t count = std::meta::annotations_of(m).size();
std::println(" {:<13} {} annotation{}", std::meta::identifier_of(m), count, count == 1 ? "" : "s");
}
} from session import run
run("""
g++-16 -std=c++26 -freflection -Wall -Wextra columns.cpp -o columns && ./columns
""") Output
$ g++-16 -std=c++26 -freflection -Wall -Wextra columns.cpp -o columns && ./columns order_id,item,qty,amount_paise id 1 annotation item 1 annotation quantity 1 annotation amount_paise 1 annotation retries 0 annotations
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 columns_session.py
Step through the copies the compiler makes of csv_header’s loop, one for each member of OrderLine:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Copy 1: id. csv_header loops over the five members of OrderLine with template for, so the compiler makes five copies of the body. In the first, column_of asks annotations_of_with_type for the Column annotations of id, finds one and extracts its name, "order_id".
- Copy 2: item. name is not nullptr, so if constexpr keeps the code that writes it. line is no longer empty, so a comma comes first, and line becomes order_id,item.
- Copy 3: quantity. The member is called quantity, but its annotation says "qty", and the annotation decides: the file gets a column named qty, and the member keeps its own name.
- Copy 4: amount_paise. The fourth copy adds ,amount_paise. Four of the five members are columns now, each name taken from the line that declares the member.
- Copy 5: retries. retries has no annotation, so column_of returns nullptr, and if constexpr drops the body of this copy. It adds no code at all, not even a test that runs later.
- The header. When the program runs, the four copies that kept code build order_id,item,qty,amount_paise, the first line the run printed. To rename a column, change its annotation; nothing else is edited.
column_of asks for the annotations of one type only, annotations_of_with_type(member, ^^Column), and returns
nullptr when there are none, so retries is left out of the file. extract<Column> turns the annotation back
into a Column value, whose name points to the static copy of the text. csv_header then needs nothing but the
struct: rename a column, add a member or skip one, and the header follows. The second half of the output lists every
annotation with annotations_of, which returns them whatever their types. Annotations come back in the order they
were written, and nothing stops a member from carrying two of the same type, so generic code has to decide which one
wins; column_of takes the first.
The whole path of one member, from its annotation to the text the program prints:
Annotations are read while compiling and become ordinary code
Text description of the diagram
The diagram follows one member of OrderLine, from columns.cpp, from top to bottom.
- The member long long amount_paise carries one annotation, Column("amount_paise").
- Inside a box titled "While compiling", the first step finds the annotation: annotations_of_with_type(m, ^^Column) returns the one Column annotation.
- The next step reads it: extract<Column> gives the value back, whose name is "amount_paise".
- The last step inside the box generates code: the header writes the name, while the member retries, which has no Column annotation, adds no code at all.
- A double-bordered box at the bottom shows the running program: the header line is order_id,item,qty,amount_paise, and the annotations themselves no longer exist.
Key idea
Generic code finds an annotation with annotations_of_with_type, turns it back into a value with extract, and
lets if constexpr drop the code of every member that has none.
Checkpoint
Check: Reading annotations back
Annotations suit facts that are fixed while compiling
Anything that describes how generic code should treat a member fits in an annotation:
| Generic code needs | The annotation holds |
|---|---|
| A column name for a file format | The name, as in Column("order_id") |
| A flag and its help text for a command line | Both texts, as in Option("--port", "port to listen on") |
| A range or a pattern for validation | The limits, or the pattern |
| A unit or a display format | The unit or the format |
| A field kept out of logs | Nothing: the marker itself is the message |
In every case the struct stays the single source of truth, and the code that reads the annotations is written once. Anyone reading a change benefits too: the rule for a member sits on the line that declares it, so a change to a column name or a limit shows up in the same diff as the change to the struct. The limits are as clear: an annotation’s value is fixed while compiling, so it cannot depend on configuration read at run time, and code that reads annotations needs a compiler with reflection.
Checkpoint
Check: What annotations suit
Practise: annotations
Nine questions on what an annotation is, how code reads it back and what it suits follow. The weakest of the three ideas comes first.
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
A CSV writer generated from annotations, and the values GCC rejects Pro: opens with a Pro, Premium or Ultimate pass
This part completes the CSV writer, with a formatting annotation and correct quoting, checks its output with an independent CSV parser, and reproduces the annotation values that GCC 16 refuses.
In this part:
- Header and rows from one annotated struct, with RFC 4180 quoting
- Which annotation values GCC accepts and rejects, reproduced
7 runnable examples (C header, C++, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
An option parser generated from a settings struct, with an exercise Pro: opens with a Pro, Premium or Ultimate pass
The same technique builds a command-line parser: one annotated struct yields the flags, the help text, the range checks and the error messages. The exercise then adds switches and required options.
In this part:
- Flags, help text and range checks from annotations
- Your turn: switches and required options
5 runnable examples (C header, C++, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Generating a new type with define_aggregate, and what reflection costs to compile Pro: opens with a Pro, Premium or Ultimate pass
Reflection can also create a type, not just code that uses one. This part generates a patch type from a struct, then measures how much compile time generated code costs compared with the same code written by hand.
In this part:
- A patch type with every member optional, generated
- What generated code costs to compile, measured
3 runnable examples (C++, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
How is a C++26 annotation such as [[=Column("id")]] different from an attribute such as [[nodiscard]]?
An attribute is a message to the compiler: [[nodiscard]] asks for a warning when a result is ignored, and program code cannot read it. An annotation, written with =, is a value for your own code. Its expression is evaluated while compiling, the resulting constant stays attached to the declaration, and reflection reads it back with annotations_of or annotations_of_with_type and extract. The value must be a constant of a structural type, the kind of value that could be a template argument. Annotations therefore suit facts that generic code needs, such as a column name, a command-line flag or a valid range, written once next to the member they describe.
-
Mid to senior
A colleague writes struct Label { std::string_view text; }; and annotates members with [[=Label{"price"}]]. GCC 16 rejects it. Explain the error, and give two designs that work, with their trade-offs.
-
Senior to staff
Design an annotation-driven CSV export for order records: column names, amounts stored in paise but written in rupees, fields that must never be exported, and correct quoting. What goes in annotations, what in the writer, and how do you test it?
-
Senior to staff
After a team adopted reflection-generated serialisers, the clean build of a 200-file target went from 6 to 9 minutes. How do you find where the time goes, and how do you bring it down without giving up the generated code?
Key takeaways
[[=value]]attaches a constant value to a declaration;annotations_of_with_typeandextractread it back while compiling.- An annotation’s value must be a constant of a structural type: no
std::string_viewmembers, and no pointers to string literals. - A
constevalconstructor that copies the text withstd::define_static_stringand keeps aconst char*to the copy turns text into a valid annotation value. - One annotated struct can drive many generated pieces of code, which stay in step with it automatically.
References
- P3394R4: Annotations for Reflection (ISO/IEC JTC1/SC22/WG21)
- P2996R13: Reflection for C++26 (ISO/IEC JTC1/SC22/WG21)
- P3491R3: define_static_{string,object,array} (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N5050) (ISO/IEC JTC1/SC22/WG21)
- RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files (RFC Editor (IETF))
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress