My little New Year’s Week project (and maybe one for you?)

[Updates: Clarified that an intrusive discriminator would be far beyond what most people mean by “C++ ABI break.” Mentioned unique addresses and common initial sequences. Added “unknown” state for passing to opaque functions.]

Here is my little New Year’s Week project: Trying to write a small library to enable compiler support for automatic raw union member access checking.

The problem, and what’s needed

During 2024, I started thinking: What would it take to make C/C++ union accesses type-checked? Obviously, the ideal is to change naked union types to something safe.(*) But because it will take time and effort for the world to adopt any solution that requires making source code changes, I wondered how much of the safety we might be able to get, at what overhead cost, just by recompiling existing code in a way that instruments ordinary union objects?

Note: I describe this in my C++26 Profiles proposal, P3081R0 section 3.7. The following experiment is trying to validate/invalidate the hypothesis that this can be done efficiently enough to warrant including in an ISO C++ opt-in type safety profile. Also, I’m sure this has been tried before; if you know of a recent (last 10 years?) similar attempt that measured its results, please share it in the comments.

What do we need? Obviously, an extra discriminator field to track the currently active member of each C/C++ union object. But we can’t just add a discriminator field intrusively inside each C/C++ union object, because that would change the size and layout of the object and be a massive link/ABI incompatibility even with C compilers and C code on the same platform which would all need to be identically updated at the same time, and it would break most OSes whose link compatibility (existing apps, device drivers, …) rely on C ABIs and APIs and use unions in stable interfaces; breaking that is much more than people usually mean by “C++ taking an ABI break” which is more about evolving C++ standard library types.

So we have to store it… extrinsically? … as-if in a global internally-synchronized map<void* /*union obj address*/, uintNN_t /*discriminator*/>…? But that sounds stupid scary: global thread safety lions, data locality tigers, and even some branches bears, O my! Could such extrinsic storage and additional checking possibly be efficient enough?

My little experiment

I didn’t know, so earlier this year I wrote some code to find out, and this week I cleaned it up and it’s now posted here:

The workhorse is extrinsic_storage<Data>, a fast and scalable lock-free data structure to nonintrusively store additional Data for each pointer key. It’s wait-free for nearly all operations (not just lock-free!), and I’ve never written memory_order_relaxed this often in my life. It’s designed to be cache- and prefetcher-friendly, such as using SOA to store keys separately so that default hash buckets contain 4 contiguous cache lines of keys. Here I use it for union discriminators, but it’s a general tool that could be considered for any situation where a type needs to store additional data members but can’t store them internally.

If you’re looking for a little New Year’s experiment…

If you’re looking for a little project over the next few days to start off the year, may I suggest one of these:

  • Little Project Suggestion #1: Find a bug or improvement in my little lock-free data structure! I’d be happy to learn how to make it better, fire away! Extra points for showing how to fix the bug or make it run better, such as in a PR or your cloned repo.

  • Little Project Suggestion #2: Minimally extend a C++ compiler (Clang and GCC are open source) as described below, so that every construction/access/destruction of a union type injects a call to my little library’s union_registry<>:: functions which will automatically flag type-unsafe accesses. If you try this, please let me know in the comments what happens when you use the modified compiler on some real world source! I’m curious whether you find true positive union violations in the union-violations.log file – of course it will also contain false positives, because real code does sometimes use unions to do type punning on purpose, but you should be able to eliminate batches of those at a time by their similar text in the log file.

To make #2 easier, here’s a simple API I’ve provided as union_registry<>, which wraps the above in a compiler-intgration-targeted API. I’ll paste the comment documentation here:

//  For an object U of union type that
//  has a unique address, when              Inject a call to this (zero-based alternative #s)
//
//    U is created initialized                on_set_alternative(&U,0) = the first alternative# is active
//
//    U is created uninitialized              on_set_alternative(&U,invalid)
//
//    U.A = xxx (alt A is assigned to)        on_set_alternative(&U,#A)
//
//    U or U.A is passed to a function by     on_set_alternative(&U,unknown)
//      pointer/reference to non-const
//      and we don't know the function
//      is compiled in this mode
//
//    U.A (alt A is otherwise used)           on_get_alternative(&U,#A)
//      and A is not a common initial
//      sequence
//
//    U is destroyed / goes out of scope      on_destroy(&U)
//
//  That's it. Here's an example:
//    {
//      union Test { int a; double b; };
//      Test t = {42};                        union_registry<>::on_set_alternative(&u,0);
//      std::cout << t.a;                     union_registry<>::on_get_alternative(&u,0);
//      t.b = 3.14159;                        union_registry<>::on_set_alternative(&u,1);
//      std::cout << t.b;                     union_registry<>::on_get_alternative(&u,1);
//    }                                       union_registry<>::on_destroy(&u);
//
//  For all unions with up to 254 alternatives, use union_registry<>
//  For all unions with between 255 and 16k-2 alternatives, use union_registry<uint16_t>
//  If you find a union with >16k-2 alternatives, email me the story and use union_registry<uint32_t>

Rough initial microbenchmark performance

My test environment:

  • CPU: 2.60 GHz i9-13900H (14 physical cores, 20 logical cores)
  • OSes: Windows 11, running MSVC natively and GCC and Clang via Fedora in WSL2

My test harness provided here:

  • 14 test runs: Each successively uses { 1, 2, 4, 8, 16 32, 64, 1, 2, 4, 8, 16, 32, 64 } threads
    • Each run tests 1 million union objects, 10,000 at a time, 10 operations on each union; the test type is union Union { char alt0; int alt1; long double alt2; };
    • Each run injects 1 deliberate “type error” failure to trigger detection, which results in a line of text written to union-violations.log that records the bad union access including the source line that committed it (so there’s a little file I/O here too)
  • Totals:
    • 14 million union objects created/destroyed
    • 140 million union object accesses (10 per object, includes construct/set/get/destroy)

On my machine, here is total the run-time overhead (“total checked” time using this checking, minus “total raw” time using only ordinary raw union access), for a typical run of the whole 140M unit accesses:

Compiler total raw (ms) total checked (ms) total overhead (ms) Notes
MSVC 19.40 -O2 ~190 ~1020 ~830 Compared to -O2, -Ox checked was the same or very slightly slower, and -Os checked was 3x slower
GCC 14 -O3 ~170 ~800 ~630 Compared to -O3, -O2 overall was only slightly slower
Clang 18 -O3 ~170 ~510 ~340 Compared to -O3, -O2 checked was about 40% slower

Dividing that by 140 million accesses, the per-access overhead is:

Compiler total overhead (ns) / total accesses average overhead / access (ns)
MSVC 830M ns / 140M accesses 5.9 ns / access
GCC (midpoint) 630M ns / 140M accesses 4.5 ns / access
Clang 340M ns / 140M accesses 2.4 ns / access

Finally, recall we’re running on a 2.6 GHhz processor = 2.6 clock cycles per ns, so in CPU clock cycles the per-access overhead is:

Compiler average overhead / access (cycles)
MSVC 15 cycles / access
GCC 11.7 cycles / access
Clang 6.2 cycles / access

This… seems too good to be true. I may well be making a silly error (or several) but I’ll post anyway so we can all have fun correcting them! Maybe there’s a silly bug in my code, or I moved a decimal point, or I converted units wrong, but I invite everyone to have fun pointing out the flaw(s) in my New Year’s Day code and/or math – please fire away in the comments.

Elaborating on why this seems too good to be true: Recall that one “access” means to check the global hash table to create/find/destroy the union object’s discriminator tag (using std::atomics liberally) and then also set or check either the tag (if setting or using one of the union’s members) and/or the key (if constructing or destroying the union object). But even a single L2 cache access is usually around 10-14 cycles! This would mean this microbenchmark is hitting L1 cache almost always, even while iterating over 10,000 active unions at a time, often with more hot threads than there are physical or logical cores pounding on the same global data structure, and occasionally doing a little file I/O to report violations.

Even if I didn’t make any coding/calculation errors, one explanation is that this microbenchmark has great L1 cache locality because the program isn’t doing any other work, and in a real whole program it won’t get to run hot in L1 that often – that’s a valid possibility and concern, and that’s exactly why I’m suggesting Little Project #2, above, if anyone would like to give that little project a try.

In any event, thank you all for all your interest and support for C++ and its evolution and standardization, and I wish all of you and your families a happier and more peaceful 2025!


(*) Today we have std::variant which safely throws an exception if you access the wrong alternative, but variant isn’t as easy to use as union today, and not as type-safe in some ways. For example, the variant members are anonymous so you have to access them by index or by type; and every variant<int,string> in the program is also anonymous == the same type, so we can’t distinguish/overload unrelated variants that happen to have similar alternatives. I think the ideal answer – and it looks like ISO C++ is just 1-2 years from being powerful enough to do this! – will be something like the safe union metaclass using reflection that I’ve implemented in cppfront, which is as easy to use as union and as safe as variant – see my CppCon 2023 keynote starting at 39:16 for a 4-minute discussion of union vs. variant vs a safe union metafunction that uses reflection.

My CppCon keynote yesterday is available on YouTube

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’re here in town at CppCon, there are over a dozen Reflection and Safety/Security talks this week that I mention in my talk. In particular, tomorrow (Wednesday) don’t miss Amanda Rousseau’s keynote on Security and in the afternoon Andrei’s own talk on Reflection, and then on Friday afternoon to cap off CppCon 2024 be there for Daveed Vandevoorde’s closing keynote on Reflection.

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.

Reader Q&A: What’s the best way to pass an istream parameter?

Here’s a super simple question: “How do I write a parameter that accepts any non-const std::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:

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:

add_from_stream( std::stringstream(some_string_data) );
    // ERROR, can't pass rvalue to A's parameter

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-const std::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…

Post-C++26: Simple, elegant, clean

Remember I mentioned Barry Revzin’s current paper P2481? Its title is “Forwarding reference to specific type/template” and it proposes this exact feature.

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):

Screenshot from CppCon 2022 showing "forward" parameter passing

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 forward std::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.

April talk video posted: “Safety, Security, Safety[sic] and C/C++[sic]”

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.

Pre-ACCU interview video is live

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)
  • Safety and C++ and cppfront topics
  • Kevin’s outro: “Get your ticket for ACCU now!”

Effective Concurrency course & upcoming talks

With the winter ISO meeting behind us, it’s onward into spring conference season!

ACCU Conference 2024. On April 17, I’ll be giving a talk on C++’s current and future evolution, where I plan to talk about safety based on my recent essay “C++ safety, in context,” and progress updates on cppfront. I’m also looking forward to these three keynoters:

  • Laura Savino, who you may recall gave an outstanding keynote at CppCon 2023 just a few months ago. Thanks again for that great talk, Laura!
  • Björn Fahller, who not only develops useful libraries but is great at naming them (Trompeloeil, I’m looking at you! [sic]).
  • Inbal Levi, who chairs one of the two largest subgroups in the ISO C++ committee (Library Evolution Working Group, responsible for the design of the C++ standard library) and is involved with organizing and running many other C++ conferences.

Effective Concurrency online course. On April 22-25, I’ll be giving a live online public course for four half-days, on the topic of high-performance low-latency coding in C++ (see link for the course syllabus). The times of 14.00 to 18.00 CEST daily are intended to be friendly to the home time zones of attendees anywhere in EMEA and also to early risers in the Americas. If you live in a part of the world where these times can’t work for you, and you’d like another offering of the course that is friendlier to your home time zone, please email Alfasoft to let them know! If those times work for you and you’re interested in high performance and low latency coding, and how to achieve them on modern hardware architectures with C++17, 20, and 23, you can register now.

Beyond April, later this year I’ll be giving talks in person at these events:

Details for the November conferences will be available on their websites soon.

I look forward to chatting with many of you in person or online this year!

Weekend update: Operator and parsing design notes

Thanks again for all the bug reports and feedback for Cpp2 and cppfront! As I mentioned last weekend, I’ve started a wiki with “Design notes” about specific aspects of the design to answer why I’ve made them they way they currently are… basic rationale, alternatives considered, in a nutshell, as quick answers to common questions I encounter repeatedly.

This weekend I wrote up three more short design notes, the first of which is the writeup on “why postfix unary operators?” that I promised in my CppCon 2022 talk.

Cpp2 design notes: UFCS, “const”, “unsafe”, and (yes) ABI

Thanks to everyone who has offered bug reports and constructive suggestions for Cpp2 and cppfront.

To answer common questions I encounter repeatedly, I’ve started a wiki with “Design notes” about specific aspects of the design to answer why I’ve made them they way they currently are… basic rationale, alternatives considered, in a nutshell. There are four design notes so far… pasting from the wiki:

  • Design note: UFCS Why does UFCS use fallback semantics (prefer a member function)? Doesn’t that mean that adding a member function later could silently change behavior of existing call sites?
  • Design note: const objects by default Should objects be const? Mostly yes.
  • Design note: unsafe code Yes, I intend that we should be able to write very-low-level facilities in Cpp2. No, that doesn’t mean a monolithic “unsafe” block… I think we can do better.
  • Design note: ABI Cpp2 is ABI-neutral, but its immunity from backward compatibility constraints presents an opportunity for link-level improvements, not just source-level improvements.

The wiki also contains links to related projects. There are two of those so far:

Thanks again for the feedback and interest.

Something I implemented today: “is void”

[Edited to add pre-publication link to next draft of P2392, revision 2, and correct iterator comparison]

Brief background

As I presented at CppCon 2021 starting at 11:15, I’m proposing is (a general type or value query) and as (a general cast, for only the safe casts) for C++ evolution. The talk, and the ISO C++ evolution paper P2392 it’s based on, explained why I hope that is and as can provide a general mechanism to power pattern matching with inspect, while conversely also liberating the power of pattern matching beyond just inspect for use generally in the language (e.g., in requires clauses, in general code). Here’s the key slide from last year, that I cited again in this year’s talk:

Note this isn’t about “making it look pretty.” is and as do lead to simpler and prettier code, but whereas human programmers love clear and consistent spellings, generic code demands that consistency. Here’s the key 1-min clip from last year, summarizing the argument in a nutshell:

Today: Divergent emptiness

In that vein, today I was catching up with some cppfront PRs, and Drew Gross pointed out that as I’ve begun implementing is and as in Cpp2 syntax, one thing didn’t work as Drew expected:

is std::nullopt_t doesn’t appear to match an empty optional

Drew Gross in cppfront PR #5

That got me thinking.

First, that this wasn’t supported in the current P2392 was intentional, because nullopt_t isn’t a “real type”… it’s a signal for “empty / no value” which until now wasn’t covered in P2392. Recall that is and as are related, including that if is T succeeds it means that a dynamic as T cast to the same type will succeed. But that’s not true for nullopt_t, which is optional‘s way of signaling an empty state; you can’t cast to “no type.”

Still, this got me thinking that testing for “empty” could be useful. And if we do provide an is test for an empty optional, it makes sense for there not to be an as cast for that, which simplifies how we think about it. And it should be spelled generically in a way that works equally for other kind of empty things, so we wouldn’t want to spell it is nullopt_t because that “empty state” name is specific to optional only. It is one of many existing divergent ad-hoc spellings we’ve added for “empty state” (just like we had lots of divergent spellings of type queries and type casts):

  • nullopt_t is the empty state for std::optional
  • nullptr_t is the empty state for raw/smart pointers
  • More generally, the default-constructed state T() is the empty state for all Pointer-like things including iterators, and this is already the way the Lifetime profile handles it: a Pointer is any type that can be dereferenced, and a default-constructed Pointer is considered its null/empty value (including that an STL iterator is treated identically to a default-constructed (null) raw or smart pointer) and you can already see this in cppfront’s cpp2util.h null test (currently at line 298). [Edited to add: Note that it turns out the Standard doesn’t make this usefully testable for STL iterators, because it says that a default-constructed STL iterator can only be reliably compared to another default-constructed one. So while the default-constructed state is indeed an “empty” state for the iterator, as far as I know there is no way to portably test whether a given STL iterator object is actually in that state. Equality testing against a default-constructed iterator may work or it may not.]
  • monostate (and arguably valueless_by_exception) is the empty state for std::variant
  • !has_value is the empty state for std::any
  • !is_ready (which has a longer spelling in today’s standard library) is the empty state for std::*future

And so we have an opportunity to unify these too, which goes beyond what I showed last year and in my previous revision of paper P2392.

But wait, on top of all that, the language itself has already had a way to spell “no type” since the 1970s: void. And even though void is not a regular type (it doesn’t work as a type in some places in the C++ type system) it works in enough of the places we need to implement is void as the generic spelling of “is empty.”

A possible convergence: is void

So today I implemented is void as a generic “empty state” test in Cpp2 syntax in cppfront. I also checked in the following Cpp2-syntax test case, which now works as self-documented — and I couldn’t resist the nod to William Tyndale:

main: () -> int = {
    p: std::unique_ptr<int> = ();
    i: std::vector<int>::iterator = ();  // see "edited to add" note above
    v: std::variant<std::monostate, int, std::string> = ();
    a: std::any = ();
    o: std::optional<std::string> = ();

    std::cout << "\nAll these cases satisfy \"VOYDE AND EMPTIE\"\n";

    test_generic(p);
    test_generic(i);
    test_generic(v);
    test_generic(a);
    test_generic(o);
}

test_generic: ( x: _ ) = {
    std::cout
        << "\n" << typeid(x).name() << "\n    ..."
        << inspect x -> std::string {
            is void = " VOYDE AND EMPTIE";
            is _    = " no match";
           }
        << "\n";
}

Note that this generic function would be impossible to write without some kind of is void unification to eliminate all of today’s non-generic divergent “empty state” queries.

Here’s the result on my machine in Ubuntu using GCC and libstdc++… I’m glad to show GCC here after having my machine’s WSL 2 subsystem quit on me on-stage so that I couldn’t show the GCC and Clang live demos in the CppCon 2022 talk (sigh!):

Implementing it ensured the implementation worked, including that it exposed where an if constexpr is needed for std::variant‘s is void test (see cpp2util.h, currently lines 610-614; note that empty is an alias for void). Once I got it working in cppfront (prototypes matter! they help us debug our proposals) I added it to the next draft of my ISO C++ proposal paper P2392 for is/as/inspect in today’s Cpp1 syntax, including the suggested implementation.

Thanks, Drew!

My CppCon 2022 talk is online: “Can C++ be 10x simpler & safer … ?”

It was great to see many of you at CppCon, in person and online! It was a really fun conference this year, and the exhibitor hall felt crowded again which was a good feeling as we all start traveling more again.

The talk I gave on Friday is now on YouTube. In it I describe my experimental work on a potential alternate syntax for C++ (aka ‘syntax 2’ or Cpp2 for short) and my cppfront compiler that I’ve begun writing to implement it.

I hope you enjoy the talk. You can find cppfront at the GitHub repo here: