Herb Sutter is an author and speaker, a technical fellow at Citadel Securities, and serves as chair of the ISO C++ standards committee and chair of the Standard C++ Foundation.
Boy, Jens Weller turns these things around quickly! Thanks again, Jens, for having me on your Meeting C++ Live show.
I’ve put a list of the questions, with timestamped links, below…
All the questions and answers, with links
00:19 What are you up to with C++ currently / what keeps you excited?
04:04 Sean Baxter has finally written up a proposal to bring borrow checking to C++, to improve safety. What are your views on his proposal and what approach is Cpp2 planning?
08:48 Is there a long-term vision for C++? How can C++ maintain its relevance in the next 20 years?
13:14 What is your favorite C++ editor/IDE when not using Microsoft Visual Studio?
17:43 Why is MSVC 2022 falling behind Clang and GCC on C++23 and C++26 features?
21:21 What is the roadmap for Cpp2? Whether it will be fit for production use?
26:30 Should the stdlib be split in two parts. One with slow changes and one with fast changes. E.g., ranges were introduced in C++20 but finished in C++23. I am still missing some features.
29:34 Are there plans to address ABIs with interfaces or other features in C++?
36:18 What is your answer to the growing complexity of C++ to be learned especially by novices? How would we teach C++ (e.g., at the university) if it gets larger and larger?
40:53 In the context of C++’s zero-cost abstractions philosophy, how do you see future proposals for making bounds checking in std::vector both safer and more efficient?
47:13 Are C++ safety initiatives arriving too late to fend off the growing adoption of Rust for “safe” low-level development?
55:25 What is the status of the profiles proposal in C++? Will some of it be part of C++26 or C++29?
57:35 The Common Package Specification, which looked very promising, seems stalled. Why is tooling in the language not a priority?
59:11 What do you think of std::execution / P2300R10? The API changed a lot across papers, and to me is quite a piece of work for library implementers to integrate.
1:04:35 Aren’t you afraid that reflection might be misused too much (e.g., use it for serialization)?
1:06:46 If local uninitialized variables are no longer UB, how will they behave? Could you please elaborate a bit on that?
1:11:30 How is the Contracts TS coming along? What are your thoughts on Contract Based Programming, in general?
1:15:56 Any chance of having type erasure (mainly std::any) in MSVC reimplemented not on top of RTTI? Unfortunately the current implementation makes it unusable in places where symbol names are left behind by RTTI.
1:17:38 What happened with the official publication of the C++23 standard?
1:22:31 Preview of my keynote next month at Meeting C++.
Time: social hour starts at 19:00 CEST, AMA starts at 20:00 CEST
I’m looking forward to your technical questions about C++26 evolution, cppfront, Rust, reflection, safety and security, concurrency and parallelism, and software in general… and optionally questions about SF/fantasy novels and cinema, but then I might ask some similar questions in return. 😇
Then next month I’m also looking forward to speaking at Meeting C++ 2024 in person in Berlin, and seeing many of you there — thanks again, Jens, for kindly inviting me to both.
Yesterday I gave the opening talk at CppCon 2024 here in Aurora, CO, USA, on “Peering Forward: C++’s Next Decade.” Thanks to the wonderful folks at Bash Films and DigitalMedium pulling out all the stops overnight to edit and post the keynote videos as fast as possible, it’s already up on YouTube right now, below!
Special thanks to Andrei Alexandrescu for being willing to come on-stage for a mini-panel of the two of us talking about reflection examples and experience in various languages! That was a lot of fun, and I hope you will find it informative.
If you aren’t able to be here at CppCon this week, you can still catch all the keynote videos (and tonight’s Committee Fireside Chat) this week and early next week on YouTube, because they’re being rush-expedited to be available quickly for everyone; just watch the CppCon.org website, where each one will be announced as soon as it’s available. And, as always, all the over 100 session videos will become freely available publicly over the coming months for everyone to enjoy.
Here’s a super simple question: “How do I write a parameter that accepts any non-conststd::istream argument? I just want an istream I can read from.” (This question isn’t limited to streams, but includes any similar type you have to modify/traverse to use.)
Hopefully the answer will be super simple, too! So, before reading further: What would be your answer? Consider both correctness and usability convenience for calling code.
.
.
OK, have you got an answer in mind? Here we go…
Spoiler summary:
Today’s Option 0 is simple and fine. To accept rvalues too, Option 2(b) is fine today. We can hopefully do it even more simply and elegantly in C++26/29.
Pre-C++11 answer
I recently received this question in email from my friend Christopher Nelson. Christopher wrote, lightly edited (and adding an “Option 0” label):
I have a function that reads from a stream:
// Call this Option 0: A single function
void add_from_stream( std::istream &s ); // A
Pause a moment — is that the answer you came up with too? It’s definitely the natural answer, the only reasonable pre-C++11 answer, and simple and clean…
Usability issue, and C++11-26 answers
… But, as Christopher points out, it does have a small usability issue, which leads to his question:
Testing code may create a temporary [rvalue] stream, and it would be nice to not have to make it a named variable [lvalue], but this doesn’t work with just the above function:
So I want to add an overload that takes an rvalue reference, like this:
void add_from_stream( std::istream &&s ); // B
// now the above call would work and call B
That nicely sets up the motivation to overload the function. [1]
The core of the emailed question is: How do we implement functions A and B to avoid duplication?
The logic is exactly the same, so I just want one function to forward to another.
Would it be better to do this:
// Option 1: Make B do the work, and have A call B
void add_from_stream( std::istream &&s ) { // B
// do work.
}
void add_from_stream( std::istream &s ) { // A
add_from_stream( std::move(s) );
}
Or this:
// Option 2: Have A do the work, and have B call A
void add_from_stream( std::istream &s ) { // A
// do work.
}
void add_from_stream( std::istream &&s ) { // B
add_from_stream( s );
}
They both seem to work, and the compiler doesn’t complain.
It’s true that both options “work” in the sense that they compile. But, as Christopher knows, there’s more to correctness than “the compiler doesn’t complain”!
Before reading further, pause and consider… how would you answer? What are the pros and cons of each option? What surprises or pitfalls might they create? Are there any other approaches you might consider?
.
.
Christopher’s email ended by actually giving some good answers, but with uncertainty:
I think [… … …] [But] I’ve been surprised lately by some experiments with rvalue references. So I thought I would check with you.
I think it’s telling that (a) he actually does have a good intuition and answer about this, but (b) rvalue references are subtle enough that he’s uncertain about it and second-guessing his correct C++ knowledge. — Perhaps you can relate, if you’ve ever said, “I’m pretty sure the answer is X, but this part of C++ is so complex that I’m not sure of myself.”
So let’s dig in..
Option 1: Have & call && (bad)
Christopher wrote:
I think the first option looks weird, and I think it may possibly have weird side-effects.
Bingo! Correct.
If Option 1 feels “weird” to you too, that’s a great reaction. Here it is again for reference, with an extra comment:
// Option 1: Make B do the work, and have A call B
void add_from_stream( std::istream &&s ) { // B
// do work.
}
void add_from_stream( std::istream &s ) { // A
add_from_stream( std::move(s) ); // <-- threat of piracy
}
In Option 1, function A is taking a modifiable argument and unconditionally calling std::move on it to pass it to B. Remember that writing std::move doesn’t move, it’s just a cast that makes its target a candidate to be moved from, which means a candidate to have its entire useful state stolen and leaving the object ’empty.’ Granted, in this case as long as function B doesn’t actually move from the argument, everything may seem fine because, wink wink, we happen to know we’re not actually going to steal the argument’s state. But it’s still generally questionable behavior to go around calling std::move on other people’s objects, without even a hint in your API that you’re going to do that to them! That’s like pranking random strangers by tugging on their backpack zippers and then saying “just kidding, I didn’t actually steal anything this time!”… it’s not socially acceptable, and even though you’re technically innocent it can still lead to getting a broken nose.
So avoid this shortcut; it sets a bad example for young impressionable programmers, and it can be brittle under maintenance if the code changes so that the argument could actually get moved from and its state stolen. Worst/best of all, I hope you can’t even check that code in, because it violates three C++ Core Guidelines rules in ES.56 and F.18, which makes it a pre-merge misdemeanor in jurisdictions that enforce the Guidelines as a checkin gate (and I hope your company does!).
Function A violates this rule (essentially, ‘only std::move from rvalue references’):
ES.56: Flag when std::move is applied to other than an rvalue reference to non-const.
Function B violates these rules:
F.18: Flag all X&& parameters (where X is not a template type parameter name) where the function body uses them without std::move.
ES.56: Flag functions taking an S&& parameter if there is no const S& overload to take care of lvalues.
It’s bad enough that we already have to teach that std::move is heavily overused (for example, C++ Core Guidelines F.48). Creating still more over-uses is under-helpful.
Option 2: Have && call & (better)
For Option 2, Christopher notes:
I imagine that the second one works because, as a parameter the rvalue reference is no longer a pointer to a prvalue, so it can be converted to an lvalue reference. Whereas, in the direct call site it is not.
Bingo! Yes, that’s the idea.
Recall Option 2, and here “function argument” mean the thing the caller passes to the parameter, and “function parameter” means its internal name and type inside the function scope:
// Option 2: Have A do the work, and have B call A
void add_from_stream( std::istream &s ) { // A
// do work.
}
void add_from_stream( std::istream &&s ) { // B
add_from_stream( s );
}
Note that function B only accepts rvalue arguments (such as temporary objects)… but once we’re inside the body of B the named parameter is now an lvalue, because it has a name! Why? Here’s a nice explanation from Microsoft Learn:
Functions that take an rvalue reference as a parameter treat the parameter as an lvalue in the body of the function. The compiler treats a named rvalue reference as an lvalue. It’s because a named object can be referenced by several parts of a program. It’s dangerous to allow multiple parts of a program to modify or remove resources from that object.
So the argument is an rvalue (at the call site), but binding it to a parameter name makes the compiler treat it as an lvalue (inside the function)… so now we can just call A directly, no fuss no muss. Conceptually, function B is implicitly doing the job of turning the argument into an lvalue, and so serves its intended purpose of expressing, “hey there, this overload set accepts rvalues too.”
And that’s… fine.
It’s still not “ideal,” though, for two reasons. First, astute readers will have noticed that this only addresses two of the three C++ Core Guidelines violations mentioned above… the new function B still violates this rule:
ES.56: Flag functions taking an S&& parameter if there is no const S& overload to take care of lvalues.
The reason this rule exists is because functions that take rvalue references are supposed to be used as overloads with const& to optimize “in”-only parameters. We could shut up the stupid checker eliminate this violation warning by adding such an overload, and mark it =delete since there’s no other real reason for it to exist (consider: how would one use a const stream?). So to get past a C++ Core Guidelines checker we would actually write this:
// Option 2, extended: To be C++ Core Guidelines-clean
void add_from_stream( std::istream &s ) { // A
// do work.
}
void add_from_stream( std::istream &&s ) { // B
add_from_stream( s );
}
void add_from_stream( std::istream const& s ) = delete; // C
The second reason this isn’t ideal is that having to write an overload set of two or three functions is annoying at best, because we’re just trying to express something that seems awfully simple: “I want a parameter that accepts any non-conststd::istream argument“… that shouldn’t be hard, should it?
Can we do better? Yes, we can, because those aren’t the only two options.
Option 3: Write a single function using a forwarding reference (a waypoint to the ideal)
In C++, without using overloading, yes we can write a parameter that accepts both lvalues and rvalues. There are exactly three options:
Pass by value (“in+copy”)… but for streams this is impossible, because streams aren’t copyable.
Pass by const& (“in”)… but for useful streams this is impossible, because streams have to be non-const to be read from (reading modifies the stream’s position).
Pass by T&& forwarding reference … … … ?
“Exclude the impossible and what is left, however improbable, must be the truth.”
A.C. Doyle, “The Fate of the Evangeline,” 1885.
Today, the C++11-26 truth is that the way to express a parameter that can take any istream argument (both lvalues and rvalues), without using overloading, is to use a forwarding reference: T&& where T is a template parameter type…
Wait! You there, put down the pitchfork. Hear me out.
Yes, this is complex in three ways…
It means writing a template. That has the drawback that the definition must be visible to call sites (we can’t ship a separately compiled function implementation, except to the extent C++20 modules enable).
We don’t actually want a parameter (and argument) to be just any type, so we also ought to constrain it to the type we want, std::istream.
Part of the forwarding parameter pattern is to remember to correctly std::forward the parameter in the body of the function. (See C++ Core Guidelines F.19.)
Here’s the basic pattern:
// Option 3(a): Single function, takes lvalue and rvalue streams
template <typename S>
void add_from_stream( S&& s )
requires std::is_same_v<S, std::istream>
{
// do work + remember to use s as "std::forward<S>(s)"
}
For reasons discussed in Barry Revzin’s excellent current proposal paper P2481 (more on that in a moment), we actually want to allow arguments that are of derived type or convertible type, so instead of is_same we would actually prefer is_convertible:
// Option 3(b): Single function, takes lvalue and rvalue streams
template <typename S>
void add_from_stream( S&& s )
requires std::is_convertible_v<S, std::istream const&>
{
// do work + remember to use s as "std::forward<S>(s)"
}
“Seriously!?” you might be thinking. “There’s no way you’re going to teach mainstream C++ programmers to do that all the time! Write a template? Write a requires clause?? And write std::forward<S>(s) with its pitfalls (don’t forget <S>)??? Madness.”
Yup, I know. You’re not wrong.
Which is why I (and Barry) believe this feature needs direct language support…
And astute readers will recall that my Cpp2 syntax already has this generalized forward parameter passing style that also works for specific types, and I designed and implemented that feature in cppfront and demo’d it on stage at CppCon 2022 (1:26:28 in the video):
See Cpp2: Parameters for a summary of the Cpp2 approach. In a nutshell: You declare “what” you want to do with the parameter, and let the compiler do the mechanics of “how” to pass it that we write by hand today. Six options, one of which is forward, are enough to cover all kinds of parameters.
As you see in the accompanying CppCon 2022 screenshot, the forward parameters compiled to ordinary C++ that followed Option 3(a) above. A few days ago, I relaxed it to compile to Option 3(b) above instead, as Barry suggested and another Cpp2 user needed (thanks for the feedback!), so it now allows polymorphism and conversions too.
So in Cpp2 today, all you write is this:
// Works in Cpp2 today (my alternative experimental simpler C++ syntax)
add_from_stream: ( forward s: std::istream ) = {
// do work.
}
… and cppfront turns that into the following C++ code that works fine today on GCC 11 and higher, Clang 12 and higher, MSVC 2019 and higher, and probably every other fairly recent C++ compiler:
auto add_from_stream(auto&& s) -> void
requires (std::is_convertible_v<CPP2_TYPEOF(s), std::add_const_t<std::istream>&>)
{
// do work + automatically adds "std::forward" to the last use of s
}
Write forward, and the compiler automates everything else for you:
Makes that parameter a generic auto&&.
Adds a requires clause to constrain it back to the specific type.
Automatically does std::forward correctly on every definite last use of the parameter.
It’s great to have this in Cpp2 as a proof of concept, but both Barry and I want to make this easy also in mainstream ISO C++ and are independently proposing that C++26/29 support the same feature, including that it would not need to be implicitly a template. Barry’s paper has been strongly encouraged, and I think the only remaining question seems to be the syntax (see the record of polls here), which could be something like one of these:
// Proposed for C++26/29 by Barry's paper and mine
// Possible syntaxes
void add_from_stream( forward std::istream s );
void add_from_stream( forward: std::istream s );
void add_from_stream( forward s: std::istream );
// or something else, but NOT "&&&"
// -- happily the committee said "No!" to that
In Cpp2, forward parameters with a specific type have proven to be more useful than I originally expected, and that’s even though they compile down to a C++ template today… one benefit of adding this feature directly into the language is that a forwarding reference to a concrete type could be easily implemented as a non-template if it’s part of C++26/29.
My hope for near-future C++ is that the simple question “how do I pass any stream I can read from, even rvalues?” will have the simple answer “as a forwardstd::istream parameter.” This is how we can simplify C++ even by adding features: By generalizing things we already have (forwarding parameters, to work also for specific types) and enabling programmers to directly express their intent (say what you mean, and the language can help you). Then even though the language got more powerful, C++ code has become simpler.
[Updated to emphasize 2(b)] In the meantime, in today’s C++, Option 0 is legal and fine, and consider Option 2(b) if you want to accept rvalues. Today, Option 3 is the only direct (and cumbersome) way to write a function that takes a stream without having to write any overloads.
Thanks again to Christopher Nelson for this question!
.
Notes
[1] For simpler cases if you’re reading a homogenous sequence of the same type, you could try taking a std::ranges::istream_view. But as far as I know, that approach doesn’t help with general stream-reading examples.
Acknowledgments: Thanks to Davis Herring, Jens Maurer, Richard Smith, Krystian Stasiowski, and Ville Voutilainen, who are all ISO C++ committee core language experts, for helping make my answer below correct and precise.
I recently got this question in email from Sam Johnson. Sam wrote, lightly edited:
Given this code, at function local scope:
int a;
a = 5;
So many people think initialization happens on line 1, because websites like cppreference defines initialization as “Initialization of a variable provides its initial value at the time of construction.”
However, I’m convinced the initialization happens on line 2, because [various good C++ books] define initialization as simply the first meaningful value that goes into the variable.
Could you please tell me which line is considered initialization?
That’s a great question. Cppreference is correct, and for all class types the answer is simple: The object is initialized on line 1 by having its default constructor called.
But (and you knew a “but” was coming), for a local object of a fundamental built-in type like int, the answer is… more elaborate. And that’s why Sam is asking, because Sam knows that the language has been kind of loose about initializing such local objects, for historical reasons that made sense at the time.
Short answer: Saying the variable gets its initial value on line 2 is completely reasonable. But note that I deliberately didn’t say “the object is initialized on line 2,” and both the code and this answer gloss over the more important problem of: “Yeah, but what about code between lines 1 and 2 that could try to read the object’s value?”
This post has three parts:
Pre-C++26, yes, this is kind of awkward. But the funniest part is how Standard describes it today, which is just begging for a little in-good-fun roasting, and so I’ll indulge.
In C++26, we make this code safe by default, thanks to Thomas Köppe! This is a Big Deal.
In my Cpp2 experiment, this problem disappears entirely, and all types are treated equally with guaranteed initialization safety. My aim is to propose this for ISO C++ itself post-C++26, so ISO C++ could evolve to remove this issue too in the future, if there’s consensus for such a change.
Let’s start with the world today, our pre-C++26 status quo…
Pre-C++26 answer: The variable is never “initialized”
For those few built-in types like int, the answer is that in this example there is no initialization at all, because (technically) neither line is an initialization. If that surprises you, consider:
Line 1 declares an uninitialized object. There is no initial value at all, explicit or implicit.
Line 2 then assigns an “initial value.” This overwrites the object’s bits and happens to give the object the same value as if its bits had been initialized that way on line 1… but it’s an assignment, not an initialization (construction).
That said, I think it’s reasonable to informally call line 2 “setting an initial value,” in the sense that it’s the first program-meaningful value put into that object. It’s not formally an initialization, but the bits end up the same, and good books can reasonably call line 2 “initializing a.”
“But wait,” I hear someone in the back saying, “I read the Standard last night, and [dcl.init] says that line 1 is a ‘default-initialization’! Therefore line 1 is an initialization!” Yes, and no, respectively. So let’s look at the Standard’s formal precise and quite funny answer, and this is truly a delightful thing to read: The Standard does say that in line 1 the object is indeed default-initialized… but, for types like int, the term “default-initialized” is defined to mean “no initialization is performed.”
(This may be a good time to mention that “the Standard is not a tutorial”… in other words, we wouldn’t read the Standard to learn the language. The Standard is quite precise about telling us what a C++ compiler does, and there’s nothing really wrong with the Standard specifying things in this way, it’s totally fine and it totally works. But it’s not written for a lay reader, and nobody would blame you if you thought that “default-initialization [means] no initialization is performed” sounds like cognitive dissonance in action, Orwellian doublethink (which is not the same thing), passive-aggressive baiting, or just garden-variety Humpty Dumptyism.)
A related question is: After line 1, has the object’s lifetime started? The good news is that yes it has… in line 1, the uninitialized object’s lifetime has indeed started, per [basic.life] paragraph 1. But don’t let’s look too closely at that paragraph’s words about “vacuous initialization,” because that’s yet another fancyspeak in the Standard for the same concept of “initialized but, ha ha, just kidding.” (Have I mentioned that the Standard isn’t a tutorial?) And of course it’s a serious problem that the object’s lifetime has started, but it hasn’t been initialized with a predictable value… that’s the worst problem of an uninitialized variable, that it can be a security risk to read from it, which has been true “undefined behavior” that could do anything, and attackers can exploit this property.
Fortunately, this is where the safety story gets significantly better, in C++26…
C++26: It gets better (really!) and safe by default
C++26 has created the new concept of erroneous behavior, which is better than “undefined” or “unspecified” because it gives us a way to talk about code that is literally “well-defined as being Just Wrong”… seriously, that’s almost a direct quote from the paper… and because it’s now well-defined it gets stripped of the security scariness of “undefined behavior.” Think of this as the Standard having a tool to turn some behavior from “scarily undefined” to merely “tsk, we know this is partly our fault because we let you write this code and it doesn’t mean what it should mean, but you really wrote a bug here, and we’re going to put some guard-rails around this pit of snakes to remove the safety risk of you falling into it by default and our NSA/CISA/NIST/EO insurance premiums going up.” And the first place that concept has been applied has been to… drum roll… uninitialized local variables.
This is a big deal, because it means that the original example’s line 1 is now still uninitialized, but since C++26 it’s “erroneous behavior” which means that when the code is built with a C++26 compiler, undefined behavior cannot happen if you read the uninitialized value. Yes, that implies a C++26 compiler will generate different code than before… it will be guaranteed to write an erroneous value the compiler knows (but that isn’t guaranteed to be one the programmer can rely on; so don’t rely on it being zero) if there’s any possibility that value might be read.
This may seem like a small thing, but it’s already a major improvement, and shows that the committee is serious about actively changing our language to be safe by default. Making more and more code safe by default is a trend you can expect to see a lot more of in C++’s medium-term future, and that’s a very welcome thing.
While you wait for your favorite C++26 compiler to add this support, you can get an approximation of this feature today with the GCC or Clang switch -ftrivial-auto-var-init=pattern or the with MSVC switch /RTC1 (run, don’t walk, to use those now if you can). They get you most of what C++26 gives, except that they may not emit a diagnostic (e.g., the Clang switch emits a diagnostic only if you’re running Memory Sanitizer).
For example, consider how this new default prevents secrets from leaking, in this program compiled with and without today’s flag (Godbolt link):
template<int N>
auto print(char (&a)[N]) { std::cout << std::string_view{a,N} << "\n"; }
auto f1() {
char a[] = {'s', 'e', 'c', 'r', 'e', 't' };
print(a);
}
auto f2() {
char a[6];
print(a); // today this likely prints "secret"
}
auto f3() {
char a[] = {'0', '1', '2', '3', '4', '5' };
print(a); // overwrites "secret" (if only just)
}
int main() {
f1();
f2();
f3();
}
Typically, all three local arrays will reuse the same stack storage, and after f1 returns the string secret is likely still sitting on the stack, waiting for f2‘s array to overlay it.
In today’s C++ by default, without -ftrivial-auto-var-init=pattern or /RTC1, f2 will likely print secret. Which is… um (looks at feet and twists a toe to pretend to erase an imaginary spot on the floor)… let’s say problematic for safety and security. As Jon would say to today’s undefined-behavior uninitialized rule, “you give C++ a bad name.”
But with GCC and Clang -ftrivial-auto-var-init=pattern, with MSVC /RTC1, and in C++26 onward by default, f2 will not leak the secret. As Bjarne has sometimes said in other contexts, but I think applies here too: “This is progress!” And to any grumpy readers who may be inclined to say, “dude, I’m used to insecure code, getting rid of insecure code by default isn’t in the spirit of C++,” well, (a) it is now, and (b) get used to it because there’s a lot more like this on the way.
Edited to add: A frequently asked question is, why not initialize to zero? That is always proposed, but it isn’t the best answer for several reasons. The main two are: (1) zero is not necessarily a program-meaningful value, so injecting it often just changes one bug into another; (2) it often actively masks the failure to initialize from sanitizers, who now think the object is initialized and so can’t see and report the error. Using an implementation-defined well-known “erroneous” bit pattern doesn’t have those problems.
But this is C++, you always have the full power to take control and get maximum performance when you need to. So yes, if you really want, C++26 will let you opt out by writing [[indeterminate]], but every use of that attribute should be challenged in every code review and require justification in the form of clear performance measurements showing that you need to override the safe default:
int a [[indeterminate]] ;
// C++26-speak for "yes please hurt me,
// I want the bad old dangerous semantics"
Post-C++26: What more could we do?
So this is where we are pre-C++26 (highlighting the most problematic lines):
// In today’s C++ pre-C++26, for local variables
// Using a fundamental type like 'int'
int a; // declaration without initialization
std::cout << a; // undefined: read of uninitialized variable
a = 5; // assignment (not initialization)
std::cout << a; // prints 5
// Using a class type like 'std::string'
string b; // declaration with default construction
std::cout << b; // prints "": read of default constructed value
b = "5"; // assignment (not initialization)
std::cout << b; // prints "5"
Note that line 5 might not print anything… it’s undefined behavior, so you’d be lucky if it’s just a matter of printing something or not, because a conforming compiler could technically generate code to erase your hard drive, invoke nasal demons, or other traditional UB nastiness.
And here is where we are starting in C++26 (differences highlighted):
// In C++26, for local variables
// Using a fundamental type like 'int'
int a; // declaration with some erroneous value
std::cout << a; // prints ? or terminates: read of erroneous value
a = 5; // assignment (not initialization)
std::cout << a; // prints 5
// Using a class type like 'std::string'
string b; // declaration with default construction
std::cout << b; // prints "": read of default constructed value
b = "5"; // assignment (not initialization)
std::cout << b; // prints "5"
The good news: Our hard drives and noses are now safe from erasure and worse in line 5. Edited to add: The implementation might print a value or terminate, but there won’t be undefined behavior.
The fine print: C++26 compilers are required to make line 4 write a known value over the bits, and they are encouraged (but are not required) to tell you line 5 is a problem.
In my Cpp2 experimental syntax, local variables of all types are defined like a: some_type = initial_value;. You can omit the = initial_value part to express that stack space is allocated for the variable but its actual initialization is deferred, and then Cpp2 guarantees initialization before use; you must do the initialization later using = (e.g., a = initial_value;) before any other use of the variable, which gives you the flexibility of doing things like using different constructors for the same variable on different branch paths. So the equivalent example is (differences from C++26 highlighted):
// In my Cpp2 syntax, local variables
// Using a fundamental type like 'int'
a: int; // allocates space, no initialization
// std::cout << a; // illegal: can't use-before-init!
a = 5; // construction => real initialization!
std::cout << a; // prints 5
// Using a class type like 'std::string'
b: string; // allocates space, no initialization
// std::cout << b; // illegal: can't use-before-init!
b = "5"; // construction => real initialization!
std::cout << b; // prints "5"
Cpp2 deliberately has no easy way to opt out and use a variable before it has been initialized. To get that effect, you’d have to have an array of raw std::bytes or similar on the stack, and do an unsafe_cast to pretend it’s a different type… which is verbose and hard to write, and that’s because I think that unsafe code should be verbose and hard to write… but you can write it (verbosely) if you really need to, because that’s core to C++: I may disapprove of unsafe code you may write in the name of performance, but I defend to the death your right to write it when you need to; C++ always lets you open the hood and take control. My aim is simply to move from “performance by default, safety always available” where safety is the thing you have to work a bit harder to get, to “safety by default, performance always available.” The metaphor I use for this is that we don’t want to take any sharp knives away from C++ programmers, because chefs sometimes need sharp knives; but when the knives are not in use we just want to keep them in a drawer you need to opt into opening, instead of leaving them strewn about the floor and forever be reminding people to watch where they step.
So far, I find this model is working very well, and it has the triple benefits of performance (initialization work is never done until you need it), flexibility (I can call the real constructor I want), and safety (it’s always real “initialization” with real construction, and never any use-before-initialization). I think we could have this someday in ISO C++, and I intend to bring a proposal along these lines to the ISO C++ committee in the next year or two, and I’ll be as persuasive as I can. They might love it, they might find flaws I’ve overlooked, or something else… we’ll see! In any event, I’ll be sure to report any progress here.
Over the winter and spring I spent a bunch of time building my essay “C++ safety, in context” and the related ACCU 2024 safety keynote, and on behind-the-scenes work toward improving C++ memory safety that you’ll be hearing more about in the coming months (including a lock-free data structure that’s wait-free and constant-time for nearly all operations; that was fun to develop and could be useful for making certain existing C and C++ code safer, we’ll see). Safety and simplicity are the two core things I want to try to dramatically improve in C++, and are why I’m doing my cppfront experiment, so although the above absorbed some time away from cppfront coding it all contributes to the same goal. (If you don’t know what cppfront is, please see the CppCon 2022 talk for an overview, and the CppCon 2023 talk for an update and discussion of why cppfront is pursuing this evolution strategy that’s different from other projects’. “Cpp2” is the shorthand name for my experimental “C++ syntax 2,” and cppfront is the open-source compiler that compiles it.)
So now it’s time for a cppfront update with some highlights of what’s been happening since the last time I posted about it here:
Wrote Cpp2 and cppfront documentation & started numbered releases
Cppfront 0.7.0 (Mar 16, 2024), new things include:
A “tersest” function syntax, e.g.: :(x,y) x>y
Support all C++23 and draft C++26 headers that have feature test flags
Tracked contracts changes in WG21 (e.g., P2661R1)
Generate Cpp1 postfix inc/dec in term of prefix
Allow trailing commas in lists
More CI
Cppfront 0.7.1 (Jul 10, 2024), new things include:
Added .. non-UFCS members-only call syntax
Allow x: const = init; and x: * = init; (const and pointer deduction without explicit _ placeholder)
Allow concatenated string literals
Faster compile time when doing heavy reflection and code generation
Added -quiet and -cwd
Cppfront 0.7.2 (Jul 27, 2024), new things include:
Added range operators ... and ..=
Added a compile-time @regex metafunction via reflection and code generation, by Max Sagebaum
Added support for function types (e.g., std::function< (i: inout int) -> forward std::string >)
Added support for C++23 (P2290) delimited hexadecimal escapes
Updated acknowledgments: Thank you!
Thank you to all these folks who have participated in the cppfront repo by opening issues and PRs, and to many more who participated on PR reviews and comment threads! These contributors represent people from high school and undergrad students to full professors, from commercial developers to conference speakers, and from every continent (except Antarctica… I think…).
Cpp2 and cppfront documentation
As of this spring, we now have Cpp2 and cppfront documentation! Last fall at CppCon 20243, the #1 cppfront request was “please write documentation!” So now that the language is stable, over the winter I got up and running with MkDocs via James Willett’s wonderful YouTube tutorial. Thanks to everyone who provided feedback and fixes!
Official releases
With documentation in place and a stable language, I felt it was time to start numbering releases. There have been three releases so far… here are each one’s highlights.
Cppfront 0.7.0 (Mar 16, 2024)
Initial feature set complete, including documentation. Here are a few highlights added since my September 2023 update blog post:
Added a “tersest” function syntax. Functions, including unnamed function expressions (aka lambdas), that do nothing but return an expression can now omit everything between ) and the expression. Here’s an example… note that this is a whole program, because compiling in -pure-cpp2 mode by default makes the entire standard library is available (efficiently imported as the std module if your compiler supports that):
If, like me, you’re quite used to UFCS, you can also write this to put vec. first, which means the same thing: vec.std::ranges::sort( :(x,y) x>y );
And with a using namespace std::ranges nearby, you can write just vec.sort( :(x,y) x>y ); … Now, that’s how I like to write clean C++20 code, and it is indeed (after lowering) just all-ordinary C++20 code.
Support all C++23 and draft C++26 headers that have feature test flags. When you use -import-std or -include-std to make the whole C++ standard library available, cppfront tracks the ever-growing list of standard headers so that you can always use cppfront with the latest Standard C++.
Tracked contracts changes in WG21 (e.g., P2661R1). As soon as the committee’s contracts subgroup decided to switch from notation like [[pre: expression]] to pre( expression ), I made the same change in cppfront. Note that cppfront still goes beyond the “minimum viable product” being proposed for C++23, because cppfront has always supported things like contract groups and customizable violation handlers. I also upgraded the contracts implementation to guarantee any contract’s expression will not be evaluated at all unless the contract is actually being checked, plus I provide an explicitly unevaluated group that is never evaluated to express contracts that are for static analyzers only (they can contain any grammatically valid expressions).
Generate Cpp1 postfix increment/decrement in terms of prefix. Cpp2 allows only “increment/decrement in place” operators, but when lowering to Cpp1 it generates both that and the “increment/decrement and return old value” Cpp1 operators. You can write a type in Cpp2 syntax that provides the operator once, and both versions are available to Cpp1 code that consumes the Cpp2-authored type (which is just an ordinary C++ type anyway, just written in a different syntax).
Allow trailing commas in lists. I got lots of requests for this, and Cpp2 now allows an extra redundant trailing comma at the end of any comma-delimited list. This is desirable because it lets programmers have cleaner source code diffs when adding a parameter or argument (if the source already has parameters/arguments on their own lines), and it slightly simplifies metafunctions’ code generation because they don’t have to track whether to suppress the last comma (and I think it’s important to make source code generation use cases easier). For a bit more rationale, see this Issue comment that goes with the commit. Edited to add: And the “Design note: Commas” that covers it in even more detail.
More CI: Besides build tests, added regression testing scripts and workflows. Thanks to Jaroslaw Glowacki and Johel Ernesto Guerrero Peña!
Added .. non-UFCS members-only syntax. This syntax invokes only a member function, and will not fall back to free functions. The main motivation was to provide a way to experiment if the UFCS logic noticeably added to Cpp1 compile times; I haven’t been able to verify that they cause much more compile time impact than an average-complexity line of code, but this way people can try it out. Example: mystring..append("foo");
Allow x: const = init; and x: * = init; without writing the _ type wildcard, in cases where all we want to say is that the deduced type is const or a pointer.
Allow concatenated string literals. For example, writing "Hello " "(name$)\n" now works. Note in that example that one string literal doesn’t use interpolation, and one does, and that’s fine.
Faster compile time when doing heavy reflection and code generation. I haven’t had to optimize cppfront much yet because it’s been fast in all my uses, but Max Sagebaum exercised metafunctions heavily for @regex (covered below, in 0.7.2) and made me find another ~100x performance improvement (thanks Max!). — This is the second optimization I can recall in cppfront so far, after Johel Ernesto Guerrero Peña‘s ~100x improvement last year by just changing the four uses of std::regex to find_if on a sorted vector of strings.
Added some command-line options:-quiet (thanks Marek Knápek for the suggestion!) and -cwd (thanks Fred Helmesjö for the PR!) command-line options.
Cppfront 0.7.2 (Jul 27, 2024)
This release added a few features, a couple of which are major and/or long-requested.
Added range operators ... and ..= . . I deliberately chose to make the default syntax ... mean a half-open range (like Rust, unlike Swift) so that it is bounds-safe by default for iterator ranges; for a “closed” range that includes the last value, use ..= . For example: iter1 ... iter2 is a range that safely doesn’t include iter2, as we expect for STL iterators; and 1 ..= 100 is a range of integer values that includes 100. For more, see the documentation here: ... and ..= range operators.
Added a compile-time @regex metafunction by Max Sagebaum. @regex uses compile-time reflection and source code generation via Cpp2 metafunctions to achieve compile- and run-time performance comparable to Hana Dusíková‘s groundbreaking CTRE library that uses today’s templates and other metaprogramming for the compile-time work. Thanks, Max! See the last ~10 minutes of my ACCU 2024 talk for an overview of CTRE and @regex and our initial performance results… the talk was a preview of the PR that was then merged in 0.7.2.
Add support for function types. You can now write a function signature as a type, such as to use with std::function (e.g., std::function< (i: inout int) -> forward std::string >) or to write a pointer to a function (e.g., pfn: *(i: inout int) -> forward std::string). For more, see the documentation here: Using function types. Thanks to Johel Ernesto Guerrero Peña for all his contributions toward function types in particular, and for his scores of merged PRs over the past year!
Added support for C++23 (P2290) delimited hexadecimal escapes. Didn’t notice these were in C++23? Neither did I! Thanks to Max Sagebaum for the PR to add support for escapes of the form \x{62}… you can now use them in Cpp2 code and they’ll work if your Cpp1 compiler understands them. Note that cppfront itself and its generated code don’t have a dependency on them; the cppfront philosophy is to “rely only on C++20 [in cppfront and its code gen requirements] for compatibility and portability, and support C++23/26 as a ‘light-up’ [people can use those features in their own Cpp2 code and they work if their Cpp1 compiler supports them].”
What’s next
For the rest of the year, I plan to:
Have regular cppfront releases, and post an update here for each one.
Share some updates about the cppfront license (currently it’s a non-commercial-use-only CC license to emphasize that this has been a personal experiment).
But first, in just seven (7) weeks, I’ll be at CppCon to give a talk that I plan to be in three equal parts:
C++26 progress, and why C++26 is shaping up to be the most important C++ release since C++11.
C++ memory safety problems and solutions (update of my ACCU talk).
On Saturday, the ISO C++ committee completed its fourth meeting of C++26, held in St Louis, MO, USA.
Our host, Bill Seymour, arranged for high-quality facilities for our six-day meeting from Monday through Saturday. We had over 180 attendees, about two-thirds in-person and the others remote via Zoom, formally representing over 20 nations. At each meeting we regularly have new attendees who have never attended before, and this time there were nearly 20 new first-time attendees, mostly in-person. To all of them, once again welcome!
We also often have new nations joining, and this time we welcomed participants formally representing Kazakhstan and India for the first time. We now have 29 nations who are regular formal participants: Austria, Bulgaria, Canada, China, Czech Republic, Denmark, Finland, France, Germany, Iceland, India, Ireland, Israel, Italy, Japan, Kazakhstan, Republic of Korea, The Netherlands, Norway, Poland, Portugal, Romania, Russia, Slovakia, Spain, Sweden, Switzerland, United Kingdom, and United States.
Here is a Saturday group photo of the in-person attendees right after the meeting adjourned (some had already left to catch flights). Thanks to John Spicer for taking this photo. Our host Bill Seymour is seated in the front row with the yellow dot. Thank you very much again, Bill, for having us!
The committee currently has 23 active subgroups, 16 of which met in parallel tracks throughout the week. Some groups ran all week, and others ran for a few days or a part of a day and/or evening, depending on their workloads. You can find a brief summary of ISO procedures here.
This time, the committee adopted the next set of features for C++26, and made significant progress on other features that are now expected to be complete in time for C+26.
Three major features made strong progress:
P2300 std::execution for concurrency and parallelism was formally adopted to merge into the C++26 working paper
P2996 Reflection was design-approved, and is now in specification wording review aiming for C++26
P2900 Contracts made considerable progress and has a chance of being in C++26
P2300 std::execution formally adopted for C++26
The major feature approved to merge into the C++26 draft standard was P2300 “std::execution” (aka “executors”, aka “Senders/Receivers”) by Michał Dominiak, Georgy Evtushenko, Lewis Baker, Lucian Radu Teodorescu, Lee Howes, Kirk Shoop, Michael Garland, Eric Niebler, and Bryce Adelstein Lelbach. It had already been design-approved for C++26 at prior meetings, but it’s a huge paper so the specification wording review by Library Wording subgroup (LWG) took extra time, and as questions arose the paper had to iterate with LEWG for specific design clarifications and tweaks.
P2300 aims to support both concurrency and parallelism. The definitions I use: Concurrency means doing independent work asynchronously (e.g., on different threads, or using coroutines) so that each can be responsive and progress at its own speed. Parallelism means using more hardware (cores, vector units, GPUs) to perform a single computation faster, which is the key to re-enabling the “free lunch” of being able to ship an application executable today that just naturally runs much faster on newer hardware with more compute throughput that becomes available in the future (most of which new throughput now ships in the form of more parallelism).
For concurrency, recall that in C++20 we already added coroutines, but in their initial state they were more of a “portable toolbox for writing coroutines” than a fully integrated feature (e.g., we can’t co_await a std::future with just what’s in the standard). Since then, we knew we’d want to add libraries on top to make coroutines easier to use, including std::future integration and a std::task library, which are still in progress. One of the big reasons to love std::execution is that it works well with coroutines, and is the biggest usability improvement yet to use the coroutine support we already have.
[Edited to add:] Eric Niebler provided three examples and descriptions I want to include here:
Example #2: cooperative multitasking with coroutines (Godbolt). Same, but using P2300 coroutines support and a third-party task type. The only allocations are for the coroutine frames themselves. This example uses the fact that awaitables are implicitly senders, and senders can be awaited in coroutines.
Example #3: multi-producer multi-consumer tasking system (Godbolt). Spins up a user-specified number of producer std::jthreads that schedule work onto the system execution context until they have been requested to stop. Clean shutdown using async_scope, and all producer threads are implicitly joined when main() returns.
The above three example illustrate several techniques, described by Eric:
* how to cooperatively multitask on an embedded system that has only one thread and no allocator
* how to implement a multi-producer, multi-consumer tasking system
* how to use P2300 together with coroutines
* how to write a custom sender algorithm
* how to use P2300 components together with a third party library providing standard-conforming extensions
* how to spawn a variable amount of work and wait for it all to complete using the proposed async_scope from P3149
* how to use the proposed ABI-stable system context from P2079 to avoid oversubscribing the local host
Ville Voutilainen reports writing a concurrent chunked HTTPS download that integrates nicely with C++20 coroutines’ co_await and a Qt UI progress bar, using P2300’s reference implementation (plus a sender-aware task type which is expected to be standardized in the future, but third-party ones like the exec::task below work today), together with his own Qt adaptation code (about 180 lines, which will eventually ship as part of Qt). The code is short enough to show here:
For parallelism, see the December 2022 HPC Wire article “New C++ Sender Library Enables Portable Asynchrony” by Eric Niebler, Georgy Evtushenko, and Jeff Larkin, which describes the cross-platform parallel performance of std::execution. “Cross-platform” means across different parallel programming models, using both distributed-memory and shared-memory, and across different computer architectures. (HT: Thanks Mark Hoemmen and others for reminding us about this article.) The NVIDIA coauthors of P2300 report that parallel performance is on par with CUDA code.
Mikael Simberg reports that another parallelism example from the HPC community to show off P2300 is DLA_Future (GitHub), which implements a distributed CPU/GPU eigensolver. It optionally uses std::execution’s reference implementation, and plans to use std::execution unconditionally once it ships in C++26 standard libraries. In that repo, one advanced example is this distributed Cholesky decomposition code (GitHub) (note: it still uses start_detached which was recently removed, and plans to use async_scope once available).
See also P2300 itself for more examples of both techniques.
Reflection design-approved for C++26
The Language Evolution working group (EWG) approved the design of the reflection proposal P2996R2 “Reflection for C++26” by Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde and it has now begun language specification wording review, currently on track for C++26. (Updated to add: The Library Evolution working group (LEWG) is still reviewing the library part of the design.)
This is huge, because reflection (including generation) will be by far the most impactful feature C++ has ever added since C++98, and it will dominate the next decade and more of C++ usage. It’s a watershed event; a sea change in C++. I say this for three reasons:
First, reflection and generation are the biggest power tool C++ has ever seen to improve library building: It will enable writing C++ libraries that were infeasible or impossible before, and its impact on writing libraries will likely be bigger than all the other library-writing improvements combined that the language has added from C++11 until now (e.g., lambdas, auto, if constexpr, requires, type traits).
Second, reflection and generation will simplify C++ language evolution: It will reduce the need to add as many future one-off or “narrow” language extensions to C++, because we will be able to write many of them as compile-time libraries in ordinary C++ consteval code using reflection and generation. That by itself will help slow down the future growth of complexity of the language. And it has already been happening; in recent years, SG7 (the subgroup responsible for compile-time programming) has redirected some narrow language proposals to explore how to write them using reflection instead.
Third, reflection and generation is the foundation for another potential way to dramatically simplify how we write C++ code, namely my metaclasses proposal which is “just” a small (but powerful) thin extension layered on top of reflection and generation… for details, see the Coda at the end of this post.
Still aiming for C++26 timeframe: Contracts
We spent four full days of subgroup time on the contracts proposal P2900 “Contracts for C++” by Joshua Berne, Timur Doumler, Andrzej Krzemieński, Gašper Ažman, Tom Honermann, Lisa Lippincott, Jens Maurer, Jason Merrill, and Ville Voutilainen: One and a half days in language design (EWG) on Monday afternoon and Tuesday, a parallel session in the safety group (SG23) on Tuesday, two days in the Contracts subgroup (SG21) on Wednesday and Thursday, then a quarter-day back in EWG on Friday after lunch for another session on virtual function contracts. In all, we worked through many design issues and made good progress toward consensus on several of them. We still have further work to do in order to build consensus on other open design questions, but the consensus is gradually improving and the list of open questions is gradually getting shorter… we’ll see! I’m cautiously optimistic that we have a 50-50 chance of getting contracts in C++26, which means that we will have to iron out the remaining differences within the next 11 months to meet C++26’s feature-freeze no-later-than hard deadline next June.
Adopted for C++26: Core language changes/features
Here are some additional highlights… note that these links are to the most recent public version of each paper, and some were tweaked at the meeting before being approved; the links track and will automatically find the updated version as soon as it’s uploaded to the public site.
The core language adopted 6 papers, including the following…
P0963R3 “Structured binding declaration as a condition” by Zhihao Yuan. This allows structured binding declarations with initializers appearing in place of the conditions in if, while, for, and switch statements, so you can decompose more conveniently and take a branch only if the returned non-decomposed object evaluates to true. Thanks, Zhihao!
Adopted for C++26: Standard library changes/features
In addition to P2300 std::execution, already covered above, the standard library adopted 11 other papers, including the following…
The lowest-numbered paper approved, which means it has been “baking” for the longest time, is something some of us have been awaiting for a while: P0843R11 “inplace_vector” by Gonzalo Brito Gadeschi, Timur Doumler, Nevin Liber, and David Sankel. The paper’s overview says it all – thank you, authors!
This paper proposes inplace_vector, a dynamically-resizable array with capacity fixed at compile time and contiguous inplace storage, that is, the array elements are stored within the vector object itself. Its API closely resembles std::vector<T, A>, making it easy to teach and learn, and the inplace storage guarantee makes it useful in environments in which dynamic memory allocations are undesired.
This container is widely-used in the standard practice of C++, with prior art in, e.g., boost::static_vector<T, Capacity> or the EASTL, and therefore we believe it will be very useful to expose it as part of the C++ standard library, which will enable it to be used as a vocabulary type.
P3235R3 “std::print more types faster with less memory” by Victor Zverovich gets my vote for the “best salesmanship in a paper title” award! If you like std::print, this is more, faster, sleeker (who wouldn’t vote for that?!) by expanding the applicability of the optimizations previously delivered in P3107 which initially were applied to only built-in type and string types, and now work for more standard library types. Thanks for all the formatted I/O, Victor!
P2968R2 “Make std::ignore a first-class object” by Peter Sommerlad formally blesses the use of std::ignore on the left-hand side of an assignment. Initially std::ignore was only meant to be used with std::tie, but many folks noticed (and recommended and relied on) that on every implementation you can also use it to ignore the result of an expression by just writing std::ignore = expression;. Even the C++ Core Guidelines’ ES.48 “Avoid casts” recommends “Use std::ignore = to ignore [[nodiscard]] values.” And as of C++26, that advice will be upgraded from “already works in practice” to “officially legal.” Thank you, Peter!
Other progress
All subgroups continued progress, more of which will no doubt be covered in other trip reports. Here are a few more highlights…
SG1 (Concurrency): Discussed 24 papers, and progressed the “zap” series of papers. To the happiness of many people (including me), concurrent_queue is finally nearing completion! A concurrent queue is one of the foundational concurrency primitives sorely missing from the standard library, and it’s great to see it coming closer to landing.
SG6 (Numerics): More progress on several proposals including a quantities-and-units library.
SG7 (Compile-Time Programming): Forwarded six more papers to the main subgroups, most of them reflection-related.
SG9 (Ranges): Continued working on ranges extensions for C++26, with good progress.
SG15 (Tooling): Starting to approve sending Ecosystem papers to the main subgroups, such as metadata formats and support for build systems.
SG23 (Safety): Reviewed several different proposals for safety improvement. The group voted favorably to support P3297 “C++26 needs contracts checking” by Ryan McDougall, Jean-Francois Campeau, Christian Eltzschig, Mathias Kraus, and Pez Zarifian.
Edited to add, for completeness the other presentations were: First, Bjarne Stroustrup presented his Profiles followup paper P3274R0 “A framework for Profiles development.” Then P3297, which I called out because it was a communication to the other groups about the contracts topic that dominated the week (above). Then Thomas Köppe presented P3232R0 “User-defined erroneous behavior.” Then Sean Baxter gave an informational demo presentation (no paper yet) of his work exploring adding borrow checking to C++ in his Circle compiler.
Thank you to all the experts who worked all week in all the subgroups to achieve so much this week!
Thank you again to the over 180 experts who attended on-site and on-line at this week’s meeting, and the many more who participate in standardization through their national bodies!
But we’re not slowing down… we’ll continue to have subgroup Zoom meetings, and then in just a few months from now we’ll be meeting again in person + Zoom to continue adding features to C++26. Thank you again to everyone reading this for your interest and support for C++ and its standardization.
Coda: From reflection to metaclass functions and P0707
The reason I picked reflection and generation to be the first “major” feature from Cpp2 that I brought to the committee in 2017, together with a major application use case, in the form of my “metaclasses” paper P0707, is because it was the biggest source of simplification in Cpp2, but it was also the riskiest part of Cpp2 — it was the most “different from what we do in C++” so I was not sure the committee and community would embrace the idea, and it was the most “risky to implement” because nothing like using compile-time functions to help generate class code had ever been tried for C++.
Most of my initial version of P0707 was a plea of ‘here’s why the committee should please give us reflection and generation.’ When I first presented it to the committee at the Toronto 2017 meeting immediately following the reflection presentation, I began my presentation with something like: “Hi, I’m Herb, and I’m their customer,” pointing to the reflection presenters, “because this is about what we could build on top of reflection.” That is still true; the main reason I haven’t updated P0707 since 2019 is because I haven’t needed to… the reflection work needs to exist first, and it has been continually progressing.
Historical note: Andrew Sutton’s Lock3 reflection implementation was created for, and funded by, my project that is now called Cpp2 , but which back then was called Cppx and used the Lock3 Clang-based reflection implementation; that’s why the Lock3 implementation has been available at cppx.godbolt.org (thanks again, Matt! you’re a rock star). C++20 consteval also came directly from this work, because we realized we would need functions that must run only at compile time to deal with static reflections and generation.
Now that reflection is landing in the standard, I plan to update my P0707 paper to finish proposing metaclasses for ISO C++. P0707 metaclasses (aka type metafunctions) are actually just a thin layer on top of P2996. To see why, consider this simple code:
// Example 1: Possible with P2996
consteval { generate_a_widget_class(); }
// let’s say that executing this consteval function generates something like:
// class widget { void f() { std::cout << "hello"; } };
With P2996, Example 1 can write such a consteval function named generate_a_widget_class that can be invoked at compile time and generates (aka injects) that widget type as if it had been hand-written by the programmer at the same point in source code.
Next, let’s slightly generalize the example by giving it an existing type’s reflection as some input to guide what gets generated:
// Example 2: Possible with P2996 (^ is the reflection operator)
class gadget { /*...*/ }; // written by the programmer by hand
consteval{ M( ^gadget ); } // generates widget from the reflection of gadget
// now this generates something like:
// class widget { /* some body based on the reflection of what’s in gadget */ };
Still with just P2996, Example 2 can write such a consteval function named M that will generate the widget class as-if was hand-written by the programmer at this point in source code, but with the ability to refer to ^gadget… for example, perhaps widget will echo some or all the same member functions and member variables as gadget, and add additional things.
And, just like that, we’ve suddenly arrived at P0707 metaclasses because all the Lock3 implementation of Cpp2 (then Cppx) metaclasses did is to “package up Example 2,” by providing a syntax to apply the consteval function M to the type being defined:
// Example 3: class(M) means to apply M to the class being defined
// (not yet legal C++)
class(M) widget{ /*...*/ };
// this proposed language feature would emit something like the following:
// namespace __prototype { class widget { /*...*/ }; }
// consteval{ M( ^__prototype::widget ); } // generates widget from __prototype::widget
Historical note: My initial proposal P0707R0 proposed the syntax “M class” (e.g., interface class, value class), and the SG7 subgroup gave feedback that it preferred “class(M)” (e.g., class(interface), class(value)) to make parsing easier. I’m fine with that; the syntax is less important, what matters is getting the expressive power.
So my plan for my next revision of P0707 is to propose class(M) syntax for Standard C++ as a further extension on top of P2996 reflection, to be implemented just like Example 3 above (and as Lock3 already did since 2017, so we know it works).
Why is that so important to simplifying C++?
First, as I show in P0707, it means that we can make classes much easier and safer to write, without wrong defaults and bug-prone boilerplate code. We can stop teaching the “Rules of 0/1/3/5,” and stop teaching =delete to get rid of generated special functions we didn’t want, because when using metafunctions to write classes we’re always using a convenient word to opt into a group of defaults for the type we’re writing and can get exactly the ones we want.
Second, we can write a Java/C#-style “class(interface)” without adding a special “interface” feature to the language as a separate type category, and with just as good efficiency and usability as languages that bake interface into the language. We can add “class(value)” to invoke a C++ function that runs at compile time to get the defaults right for value types without a new language feature hardwired into the compiler. We can add class(safe_union) and class(flag_enum) and much more.
Third, as I expressed in P0707, I hope reflection+generations+metafunctions can replace Qt MOC, COM IDL, C++/CLI extensions, C++/CX IDL, all of which exist primarily because we couldn’t express things in Standard C++ that we will now be able to express with this feature. I’m responsible for some of those nonstandard technologies; I led the design of C++/CLI and C++/CX, and one of my goals for reflection+generation+metafunctions is that I hope I will never need to design such language extensions again, by being able to express them well in normal (future) Standard C++. And I’m not alone; my understanding is that many of the vendors who own technologies like the above are already eagerly planning to responsibly transition such currently-add-on technologies to a standard reflection-and-generation based implementation, once reflection and generation are widely available in C++ compilers.
If you’re interested in more example of how metafunctions can work, I strongly recommend re-watching parts of two talks:
The middle section of my CppCon 2023 talk, starting around 18:50 up to 44:00, which shows many examples that work today using my cppfront compiler to translate them to regular C++, including detailed walkthroughs of the “enum” and “union” metaclass functions (in Cpp2 syntax, but they will work just as well as a minor extension of today’s C++ syntax as described above).
My original CppCon 2017 talk starting a few minutes in (an earlier version of this talk premiered initially at ACCU 2017), which demonstrates the approach and shows the first few examples working on the early Lock3 reflection implementation. The syntax has changed slightly, but the entire talk is still super current in 2024 as the reflection and generation it relies upon is now on its way to (finally!) landing in the standard.
I’m looking forward to finally resume updating P0707 to propose adding the rest of the expressive power it describes, as an extension to today’s C++ standard syntax, built on top of Standard C++26 (we hope!) reflection and generation. I hope to bring an updated proposal to our next meeting in November. My first step will be to try writing P0707 metafunctions in P2996 syntax to validate everything is there and works as I expect. So far, the only additional reflection support I know of that I’ll have to propose adding onto today’s P2996 is is_default_accessiblity() (alongside is_public() et al.) to query whether a member has the “default” accessibility of the class, i.e., is written before any public: or protected: or private: access specifier; that’s needed by metafunctions like “interface” that want to apply defaults, such as to make functions public and pure virtual by default without the user having to write public: or virtual or =0.
Safety is very important and we’ll be working hard on that too. But I would be remiss not to emphasize that the arrival of reflection (including generation) is a sea change that will drive of our next decade and more of C++… it’s really that big a deal, a rising tide that will lift all other boats including safety and simplicity as well. Starting soon, for many years we won’t be able to go to a C++ conference whose program doesn’t heavily feature reflection… and I’m not saying that just because I know several reflection talks have been accepted for CppCon 2024 two months from now; this really will be talked about and heavily used everywhere across our industry because there’s so much goodness to learn and use in this powerful feature.
Here is a copy of the page’s additional details, including a transcript link at bottom.
The U.S. government recently released a report calling on the technical community to proactively reduce the attack surface area of software infrastructure. The report emphasized memory safety vulnerabilities, which affect how memory can be accessed, written, allocated, or deallocated.
The report cites this class of vulnerability as a common theme in the some of the most infamous cyber events, such as the Morris worm of 1988, the Heartbleed vulnerability in 2014, and the Blastpass exploit of 2023.
Herb Sutter works at Microsoft and chairs the ISO C++ standards committee. He joins the show to talk about C++ safety.
Jordi Mon Companys is a product manager and marketer that specializes in software delivery, developer experience, cloud native and open source. He has developed his career at companies like GitLab, Weaveworks, Harness and other platform and devtool providers. His interests range from software supply chain security to open source innovation. You can reach out to him on Twitter at @jordimonpmm.
Many thanks to ACCU for inviting me back again this April. It was my first time back to ACCU (and only my second trip to Europe) since the pandemic began, and it was a delight to see many ACCUers in person again for the first time in a few years.
I gave this talk, which is now up on YouTube here:
It’s an evolved version of my March essay “C++ safety, in context.” I don’t like just repeating material, so the essay and the talk each covers things that the other doesn’t. In the talk, my aim was to expand on the key points of the essay with additional discussion and data points, including new examples that came up in the weeks between the essay and the talk, and relating it to ongoing ISO C++ evolution for safety already in progress.
The last section of the talk is a Cppfront update, including some interesting new results regarding compile- and run-time performance using metafunctions. One correction to the talk: I looked back at my code and I had indeed been making the mistake of creating a new std::regex object for each use, so that accounted for some of the former poor performance. But I retested and found that mistake only accounted for part of the performance difference, so the result is still valid: Removing std::regex from Cppfront was still a big win even when std::regex was being used correctly.
I hope you find the talk interesting and useful. Thanks very much to everyone who has contributed to C++ safety improvement explorations, and everyone who has helped with Cppfront over the past year and a half since I first announced the project! I appreciate all your input and support for ISO C++’s ongoing evolution.
On Friday, I sat down with Kevin Carpenter to do a short (12-min) interview about my ACCU talk coming up on April 17, and other topics.
Apologies in advance for my voice quality: I’ve been sick with some bug since just after the Tokyo ISO meeting, and right after this interview I lost my voice for several days… we recorded this just in time!
Kevin’s questions were about these topics in order (and my short answers):
Chatting about my ACCU talk topics (safety, and cppfront update)
Is it actually pretty easy to hop on a stage and talk about C++ for an hour (nope; or at least for me, not well)
In ISO standardization, how to juggle adding features vs. documenting what’s done (thanks to the Reddit trip report coauthors!)
ISO C++ meetings regularly have lots of guests, including regularly high school classes (yup, that’s a thing now)