C++ Module 18 – C++26 previews: reflection, contracts and safety
Smaller C++26 language features you can use now
Pack indexing, the _ placeholder, = delete with a reason, computed static_assert messages, variadic friends and #embed, on Clang 21 and GCC 16.
What you will learn
- Use pack indexing, placeholder variables and deleted functions that give a reason
- Write static_assert messages computed at compile time, and declare a pack of friends
- Compile a file's bytes into a program with
- Choose which of the smaller features a code base moving to C++26 adopts first
- Predict what breaks when a C++23 code base switches to -std=c++26, and guard new features with their macros
Before you start
What you’ll learn · Before you start
What you will learn
- Use pack indexing, placeholder variables and deleted functions that give a reason
- Write static_assert messages computed at compile time, and declare a pack of friends
- Compile a file's bytes into a program with
- Choose which of the smaller features a code base moving to C++26 adopts first
- Predict what breaks when a C++23 code base switches to -std=c++26, and guard new features with their macros
Before you start
On this page
set_timeout_ms(0.25), meant as a quarter of a second, compiles in C++23 and sets a timeout of 0 ms, because the
double converts to long without a word. In C++26 one declaration turns that call into a compile error that quotes a
reason you wrote. It is one of a handful of small language features that need no new library and that Apple clang 21
and GCC 16 both accept today with -std=c++26.
Pack indexing, Ts...[N], picks the N-th type or value of a pack. Several variables may be called _ in one
scope, for values you must name but never read. = delete("reason") makes the compiler show your reason to
whoever calls a deleted function. A static_assert message may be computed at compile time. friend Ts...;
makes every type of a pack a friend. And #embed turns the bytes of a file into a list of numbers in the source.
Each one replaces a workaround: a recursive template, a (void) cast, a comment next to a deleted overload, a fixed
message, one friend line per class, a generated array.
Eight features run in one program, on both compilers
All eight in one file, built by both compilers with every warning they offer:
// Eight smaller C++26 language features in one program; both compilers accept all of them in C++26 mode.
#include <cstdio>
#include <map>
#include <string>
#include <string_view>
#include <utility>
// 1. Pack indexing: Ts...[N] is the N-th type of a pack, and args...[N] the N-th value.
template <class... Ts> using first_t = Ts...[0];
template <class... Args> auto last(Args... args) { return args...[sizeof...(args) - 1]; }
// 2. A deleted function with a reason: the compiler shows the reason to anyone who calls it.
void set_timeout_ms(long ms) { std::printf("timeout: %ld ms\n", ms); }
void set_timeout_ms(double) = delete("pass whole milliseconds as a long, not seconds as a double");
// 3. A static_assert message computed at compile time: any constant with size() and data() will do.
struct Message {
std::string_view text;
constexpr std::size_t size() const { return text.size(); }
constexpr const char* data() const { return text.data(); }
};
constexpr std::size_t buffer_size = 64;
static_assert(buffer_size >= 16, Message{"the buffer must hold at least one 16-byte record"});
// 4. Variadic friends: every type of the pack is a friend.
template <class... Readers> class Vault {
friend Readers...;
int secret = 42;
};
struct Auditor {
static int peek(const Vault<Auditor>& v) { return v.secret; }
};
// 5. A structured binding as a condition: the object is tested, then its parts are named.
struct Parsed {
int value;
bool ok;
explicit operator bool() const { return ok; }
};
Parsed parse_digit(char c) { return c >= '0' && c <= '9' ? Parsed{c - '0', true} : Parsed{0, false}; }
// 8. #embed: the bytes of a file as a list of numbers, here the three bytes of status.txt ("ok" and a newline).
constexpr unsigned char status_bytes[] = {
#embed "status.txt"
};
int main() {
first_t<int, double> n = 3;
std::printf("first_t is int: %d, last(1, 2.5, 'x') = %c\n", n, last(1, 2.5, 'x'));
set_timeout_ms(250L);
std::printf("vault secret: %d\n", Auditor::peek(Vault<Auditor>{}));
if (auto [value, ok] = parse_digit('7')) std::printf("parsed %d (ok = %d)\n", value, ok);
// 6. Placeholder variables: several variables named _ in one scope, for values you must name but never use.
std::map<std::string, int> stock{{"pens", 12}, {"pads", 4}};
int total = 0;
for (const auto& [_, count] : stock) total += count;
auto [_, inserted] = stock.insert({"ink", 7});
std::printf("total %d, inserted ink: %d\n", total, inserted);
// 7. An attribute on one structured binding.
auto [count, label [[maybe_unused]]] = std::pair{3, "unused"};
std::printf("count %d\n", count);
std::printf("status.txt: %zu bytes, starting with '%c'\n", sizeof status_bytes, status_bytes[0]);
} from session import run
# Apple clang 21 and GCC 16 in C++26 mode, with the warnings a careful project uses.
run("""
clang++ -std=c++26 -Wall -Wextra -pedantic features.cpp -o features && ./features
g++-16 -std=c++26 -Wall -Wextra -pedantic features.cpp -o features-gcc && ./features-gcc
""") Output
$ clang++ -std=c++26 -Wall -Wextra -pedantic features.cpp -o features && ./features
features.cpp:44:2: warning: #embed is a Clang extension [-Wc23-extensions]
44 | #embed "status.txt"
| ^
1 warning generated.
first_t is int: 3, last(1, 2.5, 'x') = x
timeout: 250 ms
vault secret: 42
parsed 7 (ok = 1)
total 16, inserted ink: 1
count 3
status.txt: 3 bytes, starting with 'o'
$ g++-16 -std=c++26 -Wall -Wextra -pedantic features.cpp -o features-gcc && ./features-gcc
first_t is int: 3, last(1, 2.5, 'x') = x
timeout: 250 ms
vault secret: 42
parsed 7 (ok = 1)
total 16, inserted ink: 1
count 3
status.txt: 3 bytes, starting with 'o'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 features_session.py
Both builds printed the same seven lines. first_t<int, double> is int, and last returned the last argument by
indexing the value pack. The deleted overload never ran, because nothing called it, and the Auditor could read the
vault’s private member only because the pack of friends named it. The if ran because parse_digit('7') returned an
object whose explicit operator bool was true, and the loop over the map added 12 and 4 without naming the keys. The
only diagnostic was Clang’s note that #embed is a Clang extension: Clang implements the directive, which C23 has
too, but its status page does not list it as done for C++26. GCC accepts it as standard C++26.
Preview: both compilers
Part of the final C++26 draft and accepted today by both compilers of this track: each example ran on Apple clang 21
and on GCC 16.2 with -std=c++26, and where they differ, the run shows it.
The diagram follows last(1, 2.5, 'x'): one parameter pack: A template parameter or function parameter that stands for any number of arguments, none included: typename... Ts declares a pack of types, Ts... args a pack of function parameters, and sizeof...(Ts) counts the elements. More in “Variadic templates and fold expressions” of types and one of values, each
indexed from 0:
Pack indexing picks the N-th type or value of a pack, counting from 0
Text description of the diagram
The diagram follows the call last(1, 2.5, 'x') from top to bottom.
- The call instantiates the function template with the type pack Args = {int, double, char}.
- The type pack, in three boxes: Args...[0] is int, Args...[1] is double and Args...[2] is char.
- An arrow labelled "one value per type" leads to the value pack, in three boxes: args...[0] is 1, args...[1] is 2.5 and args...[2] is 'x'.
- The last box shows the expression the function returns, args...[sizeof...(args) - 1]: the value 'x', whose type is Args...[2], char.
Each feature replaces a workaround that C++23 code still carries:
- pack indexing: The C++26 syntax Ts...[i] for the type at position i of a type pack, and args...[i] for the argument at that position, where i is a constant expression counted from 0. More in “Variadic templates and fold expressions” names the N-th element of a
pack directly, where C++23 needed a recursive template or
std::tuple_element_t<N, std::tuple<Ts...>>. - The placeholder
_may be declared again in the same block, so a lock guard, an ignored binding and an unused result can all be called_. = delete("reason")puts the reason into the error message itself, so the caller no longer has to find the comment beside the declaration.- A computed
static_assertmessage can include the values that broke the assertion, such as the size that was too small. - Variadic friends declare a whole pack of friends in one line, so a template can open its private members to the types it is given.
- A structured binding: A declaration such as auto [name, marks] = s; (C++17) that gives one name to each element of an array, a tuple-like type or a class whose data members are all public. The names refer to the members of one unnamed variable made from the initialiser: a copy with auto, s itself with auto&. More in “std::pair, std::tuple and structured bindings” as a condition tests the object, through its
explicit operator bool, and names its parts in the sameif. - An attribute on one binding, such as
[[maybe_unused]], marks that binding alone, not the whole declaration. #embedturns a file’s bytes into a comma-separated list at compile time, without a generated source file; the preprocessor lesson shows it in a longer example.
Checkpoint
Check: The eight features and what they replace
Reasons and placeholders first, the rest where they fit
Not every feature deserves a change in old code. The first two steps change only what a reader sees: a deletion
reason changes the error message and nothing else, and _ names values that are never read, so neither changes what
the program does. Computed messages and pack indexing change templates that other code instantiates, so they come next, one
template at a time. Walk through a useful order for a code base moving to C++26, step by step:
- 1. Reasons on delete
A reason on every deleted overload a caller can hit start here
Write = delete("…") with a reason wherever a caller might pick the deleted overload by mistake. Each reason saves a search through headers the next time someone gets the error, because both compilers print it after the function's name.
- 2. The placeholder _
_ for guards and ignored bindings
Call lock guards, scope guards and the parts of a structured binding you ignore _. That removes [[maybe_unused]] attributes and (void) casts from the code.
- 3. Computed messages
static_assert messages that quote the argument
In templates that other teams instantiate, build the message at compile time so that it can quote the argument that was wrong, such as the size of a buffer that was too small.
- 4. Pack indexing
Ts...[N] wherever generic code needs the N-th element
Replace recursive helpers that peel one type at a time with Ts...[N] or args...[N]. It reads better and builds faster than a recursive helper.
- 5. Where they fit
Variadic friends and #embed, where their problem exists when needed
Use friend Ts...; for a template that grants access to a list of types, and #embed for binary data such as test fixtures, certificates or shaders that would otherwise need a generator step.
The timeline has 5 events, from 1. Reasons on delete to 5. Where they fit.
Start where a change saves the next reader time, and leave variadic friends and #embed until code has the problem
they solve.
Checkpoint
Check: Which feature to adopt first
Your own words in the compiler’s error
Two of these features exist for the person who gets a compile error. The file has two deliberate mistakes, chosen
with -DMISTAKE:
// Two mistakes whose error messages C++26 lets the author of the code write. Build with -DMISTAKE=1 or -DMISTAKE=2.
#include <cstddef>
#include <string_view>
void set_timeout_ms(long ms);
void set_timeout_ms(double) = delete("pass whole milliseconds as a long, not seconds as a double");
// A message built at compile time from a template argument: the text names the size that was too small.
template <std::size_t N> struct TooSmall {
char text[64]{};
std::size_t length = 0;
constexpr TooSmall() {
append("record buffer of ");
append_number(N);
append(" bytes, need at least 16");
}
constexpr void append(std::string_view s) {
for (char c : s) text[length++] = c;
}
constexpr void append_number(std::size_t n) {
char digits[20];
int count = 0;
do digits[count++] = static_cast<char>('0' + n % 10); while (n /= 10);
while (count > 0) text[length++] = digits[--count];
}
constexpr std::size_t size() const { return length; }
constexpr const char* data() const { return text; }
};
template <std::size_t N> struct RecordBuffer {
static_assert(N >= 16, TooSmall<N>{});
char bytes[N];
};
int main() {
#if MISTAKE == 1
set_timeout_ms(0.25);
#elif MISTAKE == 2
RecordBuffer<8> buffer;
(void)buffer;
#endif
} from session import run
# Each build fails on purpose. Only the error lines are shown; each compiler adds notes below them.
run("""
clang++ -std=c++26 -DMISTAKE=1 -c diagnostics.cpp -o /dev/null
g++-16 -std=c++26 -DMISTAKE=1 -c diagnostics.cpp -o /dev/null
clang++ -std=c++26 -DMISTAKE=2 -c diagnostics.cpp -o /dev/null
g++-16 -std=c++26 -DMISTAKE=2 -c diagnostics.cpp -o /dev/null
""", keep=r"error:") Output
$ clang++ -std=c++26 -DMISTAKE=1 -c diagnostics.cpp -o /dev/null diagnostics.cpp:37:3: error: call to deleted function 'set_timeout_ms': pass whole milliseconds as a long, not seconds as a double [9 more lines not shown] [exit status 1] $ g++-16 -std=c++26 -DMISTAKE=1 -c diagnostics.cpp -o /dev/null diagnostics.cpp:37:17: error: use of deleted function 'void set_timeout_ms(double)': pass whole milliseconds as a long, not seconds as a double [9 more lines not shown] [exit status 1] $ clang++ -std=c++26 -DMISTAKE=2 -c diagnostics.cpp -o /dev/null diagnostics.cpp:31:17: error: static assertion failed due to requirement '8UL >= 16': record buffer of 8 bytes, need at least 16 [6 more lines not shown] [exit status 1] $ g++-16 -std=c++26 -DMISTAKE=2 -c diagnostics.cpp -o /dev/null diagnostics.cpp:31:19: error: static assertion failed: record buffer of 8 bytes, need at least 16 [7 more lines not shown] [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 diagnostics_session.py
Step through the first mistake, with and without the deleted overload:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- A quarter of a second. A caller writes set_timeout_ms(0.25), meaning a quarter of a second. The function takes whole milliseconds as a long.
- Only the long overload. With only set_timeout_ms(long) declared, the double converts to long without a word, and 0.25 becomes 0.
- A timeout of 0 ms. The call compiles and sets a timeout of 0 milliseconds: the bug the deleted overload exists to stop.
- Add a deleted overload. Declare set_timeout_ms(double) = delete("pass whole milliseconds as a long, not seconds as a double"). A deleted function still takes part in overload resolution.
- The exact match wins. For a double argument the deleted overload is an exact match, and the long one needs a conversion, so overload resolution picks the deleted one.
- An error with your reason. Calling a deleted function is an error. Clang said call to deleted function 'set_timeout_ms' and GCC use of deleted function, and both printed the reason after the name.
- What C++23 could say. Before C++26 the error could only say that the function was deleted; the reason was a comment the caller had to find next to the declaration.
The second mistake instantiates RecordBuffer<8>, and the message was built by TooSmall<8> during compilation:
“record buffer of 8 bytes, need at least 16”, where C++23 could only say a fixed text. Clang also prints the
condition with its values, 8UL >= 16; GCC prints the message alone.
Key idea
A deletion reason and a computed static_assert message turn a compile error into documentation: the caller reads
what to do, in your words, at the line that broke.
Checkpoint
Check: Error messages you write yourself
Practise: the smaller language features
These questions cover the eight features, the order in which to adopt them, and the errors whose words you write.
Practice that adapts to you
Practise this lesson
11 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Each feature in depth: rules, traps and costs, measured Pro: opens with a Pro, Premium or Ultimate pass
Small features have small rules, and each rule shows up as a compile error the first time it is broken. This part
reproduces the traps of pack indexing and of _ on both compilers, and measures what pack indexing saves at compile
time against the two older ways.
In this part:
- Two pack-indexing traps, reproduced on both compilers
- What pack indexing saves at compile time, measured
- The rules of the placeholder, and its one error
- Variadic friends in practice: a passkey with a list of holders
- Messages as documentation: writing them well
8 runnable examples (C++, Python) · about 10 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).
Moving a C++23 code base to C++26: what breaks, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Switching a project to -std=c++26 adds features, and it also removes things that were deprecated for years. This part
compiles five such constructs in both modes on both compilers, shows how to guard new features with their macros, and
ends with a refactoring exercise checked by sample tests.
In this part:
- Five constructs C++26 rejects or warns about, compiled both ways
- Guarding new features with their macros
- Your turn: refactor a C++23 report program
6 runnable examples (C++, Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher to mid · Warm-up
What does pack indexing, Ts...[N], do in C++26, and what did code use before it?
It names the N-th element of a pack directly, counting from 0. For a type pack, Ts...[0] is the first type; for a function parameter pack, args...[1] is the second argument, with the declared type of that parameter. N must be a constant expression, and an index past the end is a compile error that names the index and the size of the pack. Before C++26, code got the N-th type with a recursive template that peeled one type at a time, or with std::tuple_element_t<N, std::tuple<Ts...>>, and the N-th value with std::get<N>(std::forward_as_tuple(args...)). Pack indexing is shorter, reads like an array index, and compiles faster than the recursive version, because the compiler picks the element without instantiating a chain of templates. Apple clang 21 and GCC 16 both support it in C++26 mode.
-
Mid to senior
A caller passes 0.5 to set_timeout_ms(long), meaning half a second, and gets a timeout of 0 ms. How do you make such calls fail at compile time with a clear message in C++26, and how does that compare with what C++23 offered?
-
Senior to staff
Your team moves a large C++23 code base to -std=c++26 with Apple clang 21 and GCC 16. What breaks first, how do you find it across the whole code base, and how do you keep both modes building during the transition?
-
Mid to senior
Compare three ways to get the N-th type of a template parameter pack: a recursive template, std::tuple_element_t and C++26 pack indexing. Which would you use in a library today, and how would you measure the difference?
Key takeaways
- Pack indexing,
_,= delete("reason"), computedstatic_assertmessages and variadic friends work on both compilers in C++26 mode;#embedworks on both, and Apple clang 21 calls it an extension. - Adopt deletion reasons and the placeholder
_first, then computed messages and pack indexing, and variadic friends and#embedwhere their problem exists. - Write reasons and messages for the caller: both compilers print a deletion reason after the function’s name, and a
computed
static_assertmessage can quote the argument that was wrong.
References
- P2662R3: Pack Indexing (ISO/IEC JTC1/SC22/WG21)
- P2169R4: A nice placeholder with no name (ISO/IEC JTC1/SC22/WG21)
- P2573R2: = delete("should have a reason"); (ISO/IEC JTC1/SC22/WG21)
- P2741R3: User-generated static_assert messages (ISO/IEC JTC1/SC22/WG21)
- P2893R3: Variadic friends (ISO/IEC JTC1/SC22/WG21)
- P1967R14: #embed - a simple, scannable preprocessor-based resource acquisition method (ISO/IEC JTC1/SC22/WG21)
- P2809R3: Trivial infinite loops are not Undefined Behavior (ISO/IEC JTC1/SC22/WG21)
- P2795R5: Erroneous behaviour for uninitialized reads (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N5050, the C++26 final draft): [temp.variadic], [basic.scope.scope], [dcl.fct.def.delete], [dcl.pre], [cpp.embed], [diff.cpp23] (ISO/IEC JTC1/SC22/WG21)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress