Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: An annotation is a value attached to a declaration · Reflection reads annotations back while compiling · Annotations suit facts that are fixed while compiling · Practise: annotations. With a pass: 3 in-depth parts and 3 interview answers, about 16 minutes.

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

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

  1. Question 1 of 2 Who reads [[nodiscard]], and who reads [[=Column("qty")]]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The compiler reads the attribute; your own code reads the annotation, through reflection

    Annotations look like attributes but do a different job. [[nodiscard]] asks the compiler for a warning; the expression after = in an annotation is evaluated while compiling, and its result stays attached to the declaration for reflection to find.

  2. Question 2 of 2 Which of these could be the value of an annotation?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A struct whose members are all public ints
    • A const char* made by std::define_static_string
    • An int

    An annotation's value must be a constant that could also be a template argument: a scalar, or a class whose data members are all public and structural. For text, a consteval constructor that keeps a pointer from std::define_static_string is the usual answer.

Reflection reads annotations back while compiling

columns.cpp C++ · columns.cpp
// 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");
    }
}
A header made from four annotations Python · columns_session.py
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:

Interactive · stage by stage csv_header<OrderLine>(), one copy per member Step through the five copies the compiler made of the loop body. The sub-labels say what each copy adds to the header.
csv_header<OrderLine>(), one copy per member, step 6 of 6. Showing Copy 1: id (adds order_id), Copy 2: item (adds ,item), Copy 3: quantity (adds ,qty), Copy 4: amount_paise (adds ,amount_paise), Copy 5: retries (no Column: no code), The header line. In focus: The header line.Copy 1: idadds order_idCopy 2: itemadds ,itemCopy 3: quantityadds ,qtyCopy 4: amount_paiseadds ,amount_paiseCopy 5: retriesno Column: no codeThe header linefour columns, one line

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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".
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

An annotated member is found, read and turned into code while compiling; the running program only prints text.1. Annotate the member[[=Column("amount_paise")]]long long amount_paise;While compiling5. When the program runsthe header is order_id,item,qty,amount_paise;the annotations are gone2. Findannotations_of_with_type(m, ^^Column) returnsthe Column annotation3. Readextract<Column>(...)gives the value back:name "amount_paise"4. Generatethe header writes thename; retries, with noColumn, adds no code

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.

  1. The member long long amount_paise carries one annotation, Column("amount_paise").
  2. Inside a box titled "While compiling", the first step finds the annotation: annotations_of_with_type(m, ^^Column) returns the one Column annotation.
  3. The next step reads it: extract<Column> gives the value back, whose name is "amount_paise".
  4. 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.
  5. 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

  1. Question 1 of 2 OrderLine has this member at the end. What does csv_header<OrderLine>() do with it?

    Read the code, then choose one answer.

    int retries;  // no annotation
    Show the answer: Question 1 of 2

    Answer: It leaves it out, because column_of returns nullptr for a member without a Column annotation

    column_of asks only for Column annotations; with none, it returns nullptr, and the if constexpr in csv_header skips the member. The header in the run has four columns for five members.

  2. Question 2 of 2 A member carries two annotations, [[=Column("a"), =Column("b")]]. What does csv_header write for it?

    Choose one answer.

    A hint

    Annotations come back in the order they were written.

    Show the answer: Question 2 of 2

    Answer: a, because column_of takes the first Column annotation it is given

    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.

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

  1. Question 1 of 2 A team wants the column names to come from a configuration file read at start-up. Can annotations do that?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No; an annotation's value is fixed while compiling, so names known only at run time need another mechanism

    Annotations are constants of the program. They suit facts that belong to the code, such as default column names; anything chosen at run time, such as names from a file, has to be applied by ordinary code when the program runs.

  2. Question 2 of 2 Which of these is a good fit for an annotation?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The name a member's column has in an export file

    Anything that describes how generic code should treat a member fits: a column name, a flag and its help text, a range, a unit, a marker that keeps a field out of logs. Values that change while the program runs do not.

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.

  1. Question 1 of 9 What can reflection read from [[=Column("qty")]] that it cannot read from [[nodiscard]]?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: A value, Column("qty"), evaluated while compiling and attached to the declaration

    An attribute is a message to the compiler. An annotation is a constant value that stays attached to the declaration, where annotations_of and annotations_of_with_type find it and extract turns it back into a value.

  2. Question 2 of 9 Why does Column store a pointer made by std::define_static_string instead of a std::string_view?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: An annotation must be a constant of a structural type, and std::string_view has private members, so it is not one

    Structural types are those that could be template arguments: scalars, and classes whose members are all public and structural. A const char* to the static copy made by define_static_string qualifies; a std::string_view does not, and neither does a pointer straight to a string literal.

  3. Question 3 of 9 Where does an annotation go on an enumerator?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: After the enumerator's name, as in low [[=Label("Low")]]

    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.

  4. Question 4 of 9 How can one declaration carry two annotations?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: In one pair of brackets, separated by commas, or in two pairs

    A declaration may carry several annotations, in one pair of brackets or several, and generic code then decides which ones it reads.

  5. Question 5 of 9 What does std::meta::extract<Column>(notes[0]) give back in column_of?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: The annotation's value as a Column object, whose name points to the static copy of the text

    annotations_of_with_type(member, ^^Column) returns the reflections of the member's Column annotations, and extract<Column> turns one of them back into a Column value.

  6. Question 6 of 9 You change the annotation of quantity from Column("qty") to Column("quantity_ordered"). What else must change for the header to follow?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Nothing; csv_header reads the annotation while compiling

    csv_header needs nothing but the struct: rename a column, add a member or skip one, and the header follows.

  7. Question 7 of 9 Which query returns every annotation of a member, whatever its type?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: annotations_of

    annotations_of returns all of a member's annotations in the order they were written; annotations_of_with_type returns only those of one type, which is what column_of needs.

  8. Question 8 of 9 What can an annotation's value never depend on?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: Configuration read while the program runs

    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.

  9. Question 9 of 9 Why does a change to a column name show up in the same code change as the struct it belongs to?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The rule sits on the line that declares the member

    The struct stays the single source of truth, and the code that reads the annotations is written once. A change to a column name or a limit appears next to the member it concerns.

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.

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.

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.

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

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

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

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • [[=value]] attaches a constant value to a declaration; annotations_of_with_type and extract read it back while compiling.
  • An annotation’s value must be a constant of a structural type: no std::string_view members, and no pointers to string literals.
  • A consteval constructor that copies the text with std::define_static_string and keeps a const 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.