References, simply

References are for parameter passing, including range-for. Sometimes they’re useful as local variables, but pointers or structured bindings are usually better. Any other use of references typically leads to endless design debates. This post is an attempt to shed light on this situation, and perhaps reduce some of the time spent on unresolved ongoing design debates in the C++ community. Thank you to the following for their feedback on drafts of this material: Howard Hinnant, Arthur O’Dwyer, Richard Smith, Bjarne Stroustrup, Ville Voutilainen.


References

What references are and how to use them

In C++, a C& or C&& reference is an indirect way to refer to an existing object. Every reference has a dual nature: It’s implemented under the covers as a pointer, but semantically it usually behaves like an alias because most uses of its name automatically dereference it. (Other details are not covered here, including the usual parameter passing rules and that C&& has a different meaning depending on whether C is a concrete type or a template parameter type.)

C++ references were invented to be used as function parameter/return types, and that’s what they’re still primarily useful for. Since C++11, that includes the range-for loop which conceptually works like a function call (see Q&A).

Sometimes, a reference can also be useful as a local variable, though in modern C++ a pointer or structured binding is usually better (see Q&A).

That’s it. All other uses of references should be avoided.


Advanced note for experts

Please see the Q&A for const& lifetime extension, pair<T&, U&>, and especially optional<T&>.





Appendix: Q&A

Historical question: Can you elaborate a little more on why references were invented for function parameter/return types?

Here is a summary, but for more detail please see The Design and Evolution of C++ (D&E) section 3.7, which begins: “References were introduced primarily to support operator overloading…”

In C, to pass/return objects to/from functions you have two choices: either pass/return a copy, or take their address and pass/return a pointer which lets you refer to an existing object.

Neither is desirable for overloaded operators. There are two motivating use cases, both described in D&E:

  • The primary use case is that we want to pass an existing object to an operator without copying it. Passing by reference lets calling code write just a - b, which is natural and consistent with built-in types’ operators. If we had to write &a - &b to pass by pointer, that would be (very) inconvenient, inconsistent with how we use the built-in operators, and a conflict when that operator already has a different meaning for raw pointers as it does in this example.
  • Secondarily, we want to return an existing object without copying it, especially from operators like unary * and []. Passing by reference lets calling code write str[0] = 'a'; which is natural and consistent with built-in arrays and operators. If we had to write *str[0] = 'a'; to return by pointer, that would be (slightly) inconvenient and also inconsistent with built-in operators, but not the end of the world and so this one is only a secondary motivating case.

Those are the only uses of references discussed in D&E, including in the section on smart references and operator., and the only places where references are really needed still today.

What was that about range-for being like a function call?

The C++11 range-for loop is semantically like function parameter passing: We pass a range to the loop which takes it as if by an auto&& parameter, and then the loop passes each element in turn to each loop iteration and the loop body takes the element in the way it declares the loop element variable. For example, this loop body takes its element parameter by const auto&:

// Using range-for: The loop variable is a parameter to
// the loop body, which is called once per loop iteration
for (const auto& x : rng) { ... }

If we were instead using the std::for_each algorithm with the loop body in a lambda, the parameter passing is more obvious: for_each takes the range via an iterator pair of parameters, and then calls the loop body lambda passing each element as an argument to the lambda’s parameter:

// Using std::for_each: Basically equivalent
for_each (begin(rng), end(rng), [&](const auto& x) { ... });

Is a reference a pointer to an object, or an alternate name for the object?

Yes — it is either or both, depending on what you’re doing at the moment.

This dual nature is the core problem of trying to use a reference as a general concept: Sometimes the language treats a reference as a pointer (one level of indirection), and sometimes it treats it as an alias for the referenced object (no level of indirection, as if it were an implicitly dereferenced pointer), but those are not the same thing and references make those things visually ambiguous.

When passing/returning an object by reference, this isn’t a problem because we know we’re always passing by pointer under the covers and when we use the name we’re always referring to the existing object by alias. That’s clear, and references are well designed for use as function parameter/return types.

But when trying to use references elsewhere in the language, we have to know which aspect (and level of indirection) we’re dealing with at any given time, which leads to confusion and woe. References have never been a good fit for non-parameter/return uses.

Aren’t local references useful because of lifetime extension?

We “made it useful” as an irregular extension, but that’s brittle and now basically unnecessary as of C++17.

A brief history of lifetime extension: After references were first added in the 1980s, C++ later added a special case where binding a temporary object to a local variable of type const& and still later auto&& (but not generally other kinds of local references) was “made useful” by imbuing only those references with the special power of extending the lifetime of a temporary object, just because we could (and because there were use cases where it was important for performance, before C++17 guaranteed copy elision). However, these cases have always been:

  • brittle and inconsistent (e.g., const T& t = f(); and const T& t = f().x; and struct X { const T& r; } x = { f() }; extend the lifetime of an object returned by value from f(), but const T& t = f().g(); does not);
  • irregular (e.g., T& t = f(); is ill-formed, whereas const T& t = f(); and T t = f(); still uniformly work); and
  • unnecessary now that C++17 has guaranteed copy elision (e.g., just write T t = f(); and the meaning is both obvious and correct, as well as way easier to teach and learn and use).

Aren’t local references useful to get meaningful names for parts of an object returned from a function?

Yes, but since C++17 structured bindings are strictly better.

For example, given a set<int> s and calling an insert function that returns a pair<iterator, bool>, just accessing the members of the pair directly means putting up with hard-to-read code:

// accessing the members of a pair directly (unmeaningful names)
auto value = s.insert(4);
if (value.second) {
    do_something_with(value.first);
}

Structured bindings lets us directly name the members — note that this just invents names for them, it does not create any actual pointer indirection:

// using structured bindings (easy to use meaningful names)
auto [position, succeeded] = s.insert(4);
if (succeeded) {
    do_something_with(position);
}

In the olden days before structured bindings, some people like to use references to indirectly name the members — which like the above gives them readable names, but unlike the above does create new pointer-equivalent indirect variables and follows those pointers which can incur a little space and time overhead (and also isn’t as readable)…

// using references (cumbersome, don't do this anymore)
auto value      = s.insert(4);
auto& position  = value.first;          // equivalent to pointers
auto& succeeded = value.second;
if (succeeded) {                        // invisible dereference
    do_something_with(position);        // invisible dereference
}

// or using pointers (ditto)
auto value     = s.insert(4);
auto position  = &value.first;          // self-documenting pointers
auto succeeded = &value.second;
if (*succeeded) {                       // visible dereference
    do_something_with(*position);       // visible dereference
}

… but even in the olden days, references were never significantly better than using pointers since the code is basically identical either way. Today, prefer structured bindings.

Aren’t local references useful to express aliases, for example to a member of an array or container?

Yes, though pointers can do it equivalently, it’s a style choice.

For example, this local reference is useful:

auto& r = a[f(i)];
// ... then use r repeatedly ...

Or you can equivalently use a pointer:

auto p = &a[f(i)];
// ... then use *p repeatedly ...

Isn’t T& convenient for easily expressing a pointer than can’t be rebound to another object?

Yes, though T* const does equally well.

Either is mainly useful as a local variable. (See also previous answer.)

Isn’t T& convenient for easily expressing a pointer that is not null?

Not exactly — T& lets you express a pointer that’s not-null and that can’t be rebound.

You can also express not-null by using gsl::not_null<> (see for example the Microsoft GSL implementation), and one advantage of doing it this way is that it also lets you independently specify whether the pointer can be rebound or not — if you want it not to be rebindable, just add const as usual.

What about lambda [&] capture?

[&] is the right default for a lambda that’s passed to a function that will just use it and then return (aka structured lifetime) without storing it someplace where it will outlive the function call. Those structured uses fall under the umbrella of using references as parameter/return types. For non-parameter/return uses, prefer using pointers.

What about pair<T&, U&> and tuple<T&, U&> and struct { T& t; U& u; }?

I’ve mainly seen these come up as parameter and return types, where for the struct case the most common motivation is that C++ doesn’t (yet) support multiple return values, or as handwritten equivalents of what lambda [&] capture does. For those uses, they fall under the umbrella of using references as parameter/return types. For non-parameter/return uses, prefer using pointers.

[GENERAL UMBRELLA QUESTION] But what about using a reference for ((other use not as a parameter or return type or local variable))?

Don’t. WOPR said it best, describing something like the game of trying to answer this class of question: “A strange game. The only winning move is not to play.”

Don’t let yourself be baited into even trying to answer this kind of question. For example, if you’re writing a class template, just assume (or document) that it can’t be instantiated with reference types. The question itself is a will o’ the wisp, and to even try to answer it is to enter a swamp, because there won’t be a general reasonable answer.

(Disclaimer: You, dear reader, may at this very moment be thinking of an ((other use)) for which you think you have a reasonable and correct answer. Whatever it is, it’s virtually certain that a significant fraction of other experts are at this very moment reading this and thinking of that ((other use)) with a different answer, and that you can each present technical arguments why the other is wrong. See optional<T&> below.)

All of the remaining questions are specific cases of this general umbrella question, and so have the same answer…

… But what about using a reference type as a class data member?

For the specific case of pair<T&, U&> and tuple<T&, U&> and struct { T& t; U& u; }, see the earlier answer regarding those. Otherwise:

Don’t, see previous. People keep trying this, and we keep having to teach them not to try because it makes classes work in weird and/or unintended ways.

Pop quiz: Is struct X { int& i; }; copyable? If not, why not? If so, what does it do?

Basic answer: X is not copy assignable, because i cannot be modified to point at something else. But X is copy constructible, where i behaves just as if it were a pointer.

Better answer: X behaves the same as if the member were int* const i; — so why not just write that if that’s what’s wanted? Writing a pointer is arguably simpler and clearer.

… But what about using a reference type as an explicit template argument?

Don’t, see above. Don’t be drawn into trying to answer when this could be valid or useful.

Explicitly jamming a reference type into a template that didn’t deduce it and isn’t expecting it, such as calling std::some_algorithm<std::vector<int>::iterator&>(vec.begin(), vec.end());, will be either very confusing or a compile-time error (or both, a very confusing compile-time error — try std::sort).

… But what about using a reference type for a class template specialization?

Don’t, see above. Don’t be drawn into trying to answer when this could be valid or useful.

… But wait, not even optional<T&>?

Don’t, see above. Especially not this one.

An astonishing amount of ink has been spilled on this particular question for years, and it’s not slowing down — the pre-Prague mailing had yet another paper proposing an optional<T&> as one alternative, and we’ve had multiple Reddit’d posts about it in the past few weeks (exampleexample). Those posts are what prompted me to write this post, expanding on private email I wrote to one of the authors.

Merely knowing that the discussion has continued for so many years with no consensus is a big red flag that the question itself is flawed. And if you’re reading this and think you have answer, ask yourself whether in your answer optional<T&> really IS-AN optional<T> — template specializations should be substitutable for the primary template (ask vector<bool>) and the proposed answers I’ve seen for optional<T&> are not substitutable semantically (you can’t write generic code that uses an optional<T> and works for that optional<T&>), including that some of them go so far as actually removing common functions that are available on optional<T> which clearly isn’t substitutable.

There’s a simple way to cut this Gordian knot: Simply knowing that references are for parameter/return types will warn us away from even trying to answer “what should optional<T&> do?” as a design trap, and we won’t fall into it. Don’t let yourself be baited into trying to play the game of answering what it should mean. “The only winning move is not to play.”

Use optional<T> for values, and optional<T*> or optional<not_null<T*>> for pointers.

Epilogue: But wait, what about ((idea for optional<T&>))?

If after all the foregoing you still believe you have a clear answer to what optional<T&> can mean that:

  • is still semantically IS-A substitutable for the optional<> primary template (e.g., generic code can still use it as a more general optional);
  • cannot be represented about equally well by optional<not_null<T*>>; and
  • does not already have published technical arguments against it showing problems with the approach;

then please feel free to post a link below to a paper that describes that answer in detail.

Fair warning, though: Even while reviewing this article, a world-class expert reviewer responded regarding experience with one of the world’s most popular versions of optional<T&>:

“I know that Boost has optional<T&> so I tried it for my use case … ((code example)) is a run-time error for me. I expected ((a different behavior)) and it did not. I suspect the mistake is in the ambiguity: Does assigning an optional<T&> assign through the reference, or rebind the reference?”

My answer: Exactly, the dual nature of references is always the problem.

  • If the design embraces the pointer-ness of references (one level of indirection), then one set of use cases works and people with alias-like use cases get surprised.
  • If the design embraces the alias-ness of references (no indirection), then the other set of use cases works and people with pointer-like use cases get surprised.
  • If the design mixes them, then a variety of people get surprised in creative ways.

Java object references encounter similar problems — everything is implicitly a pointer, but there’s no clean way to syntactically distinguish the pointer vs. the pointee. Being able to talk separately about the pointer vs. the pointee is an important distinction, and an important and underestimated advantage of the Pointer-like things (e.g., raw pointers, iterators, ranges, views, spans) we have in C++.

Move, simply

C++ “move” semantics are simple, and unchanged since C++11. But they are still widely misunderstood, sometimes because of unclear teaching and sometimes because of a desire to view move as something else instead of what it is. This post is an attempt to shed light on that situation. Thank you to the following for their feedback on drafts of this material: Howard Hinnant (lead designer and author of move semantics), Jens Maurer, Arthur O’Dwyer, Geoffrey Romer, Bjarne Stroustrup, Andrew Sutton, Ville Voutilainen, Jonathan Wakely.

Edited to add: Formatting, added [basic.life] link, and reinstated a “stateful type” Q&A since the question was asked in comments.


Move: What it is, and how to use it

In C++, copying or moving from an object a to an object b sets b to a’s original value. The only difference is that copying from a won’t change a, but moving from a might.

To pass a named object a as an argument to a && “move” parameter (rvalue reference parameter), write std::move(a). That’s pretty much the only time you should write std::move, because C++ already uses move automatically when copying from an object it knows will never be used again, such as a temporary object or a local variable being returned or thrown from a function.

That’s it.


Advanced notes for type authors

Copying is a const operation on a, so copy construction/assignment functions should always take their parameter by const&. Move is a noexceptnon-const operation on a, so move construction/assignment functions should always be noexcept and take their parameter by (non-const) &&.

For copyable types, move is always an optimization of copy, so only explicitly write move functions for the type if copying is expensive enough to be worth optimizing. Otherwise, you’ll either get the implicitly generated move functions, or else requests to move will automatically just do a copy instead, since copy is always a valid implementation of move (it just doesn’t exercise the non-const option).

For types that are move-only (not copyable), move is C++’s closest current approximation to expressing an object that can be cheaply moved around to different memory addresses, by making at least its value cheap to move around. (Other not-yet-standard proposals to go further in this direction include ones with names like “relocatable” and “destructive move,” but those aren’t standard yet so it’s premature to talk about them.) These types are used to express objects that have unique values or uniquely own a resource.

That’s it for what advanced users need to know.





Appendix: Q&A

Wait, that seems oversimplified… for example, doesn’t C++ let me write copy functions in ways not mentioned above, like write a copy constructor that takes by non-const reference or a move constructor that can throw?

Yes, but don’t. Such things are legal but not good — ask auto_ptr (now removed), or vector implementations that used dynamic sentinel nodes (now being removed).

How can moving from an object not change its state?

For example, moving an int doesn’t change the source’s value because an int is cheap to copy, so move just does the same thing as copy. Copy is always a valid implementation of move if the type didn’t provide anything more efficient.

Can a given type document that moving from an object always changes its state? or changes it to a known state?

Yes, move is just another non-const function. Any non-const function can document when and how it changes the object’s state, including to specify a known new state as a postcondition if it wants. For example, unique_ptr‘s .release() function is guaranteed to set the object to null — just as its move functions are guaranteed to set the source object to null.

I wrote std::move(a) but a‘s value didn’t change. Why?

Because moving from an object a can modify its value, but doesn’t have to. This is the same as any other non-const operation on a.

There are other secondary reasons, but they’re all just special cases of the above fundamental reason, which applies irrespective of whether move is just a “move it if you can/want” cast or not, or whether a move vs. copy function is actually called, or other secondary reasons.

But what about the “moved-from” state, isn’t it special somehow?

No. The state of a after it has been moved from is the same as the state of a after any other non-const operation. Move is just another non-constfunction that might (or might not) change the value of the source object.

I heard that a moved-from object is in a state where “you can call its functions that have no preconditions,” or is in a “valid but unspecified state,” is that right?

Yes, both are saying the same thing as above — the object continues to be a valid object of its type, its value might or might not have been modified. The standard library specifies this guarantee for all standard types, and all well-behaved types should do the same.

Note that this is the same state as in the following example that’s familiar to C++ programmers of all experience levels:

void f( /* and optionally const */ Thing& thing ) {  // no preconditions
    // here 'thing' is a valid object of its type
    // (aka "in a valid but unspecified state")

    // ... naturally you’ll want to know its value, so now just ask it,
    //     easy peasy, just use the object ...
}

This is not a mysterious state. It’s the ordinary state any object is in when you first encounter it.

Does “but unspecified” mean the object’s invariants might not hold?

No. In C++, an object is valid (meets its invariants) for its entire lifetime, which is from the end of its construction to the start of its destruction (see [basic.life]/4). Moving from an object does not end its lifetime, only destruction does, so moving from an object does not make it invalid or not obey its invariants.

If any non-const function on an object (including moving from it) makes the object invalid, the function has a bug.

Don’t some standard types use two-phase construction where a default-constructed object isn’t in a valid state, such as unique_ptr where if it’s null you can only use a subset of its interface with defined behavior? And move-from puts them in such an invalid state?

No, and those aren’t two-phase construction types, they’re just stateful types and move-from puts them into one of their valid states.

A two-phase construction type is typically written because people are using a non-standard C++ dialect that doesn’t have exceptions to report constructor errors, so the user has to first default-construct into a not-valid (“not-fully-formed” and not yet usable) state and then additionally call a named function (which can report errors in some non-exception way) to “construct the rest of the way” before actually using the object. After that, the default-constructed state of such types is typically one you can’t get back to later via other member functions; it’s not one of the valid states, it’s an artifact. This is not recommended when using Standard C++.

None of the standard types are like that. All standard library types keep their objects in a valid usable state during their lifetimes. If they’re default constructible, the default constructor puts them into one of their valid states.

Does “but unspecified” mean the only safe operation on a moved-from object is to call its destructor?

No.

Does “but unspecified” mean the only safe operation on a moved-from object is to call its destructor or to assign it a new value?

No.

Does “but unspecified” sound scary or confusing to average programmers?

It shouldn’t, it’s just a reminder that the value might have changed, that’s all. It isn’t intended to make “moved-from” seem mysterious (it’s not).

What about objects that aren’t safe to be used normally after being moved from?

They are buggy. Here’s a recent example:

// Buggy class: Move leaves behind a null smart pointer

class IndirectInt {
    shared_ptr<int> sp = make_shared<int>(42);
public:
    // ... more functions, but using defaulted move functions
    bool operator<(const IndirectInt& rhs) const { return *sp < *rhs.sp; }
                                                // oops: unconditional deref
    // ...
};

IndirectInt i[2];
i[0] = move(i[1]); // move leaves i[1].sp == nullptr
sort(begin(i), end(i)); // undefined behavior

This is simply a buggy movable type: The default compiler-generated move can leave behind a null sp member, but operator< unconditionally dereferences sp without checking for null. There are two possibilities:

  • If operator< is right and sp is supposed to never be null, then the class has a bug in its move functions and needs to fix that by suppressing or overriding the defaulted move functions.
  • Otherwise, if the move operation is right and sp is supposed to be nullable, then operator< has a bug and needs to fix it by checking for null before dereferencing.

Either way, the class has a bug — the move functions and operator< can’t both be right, so one has to be fixed, it’s that simple.

Assuming the invariant is intended to be that sp is not null, the ideal way to fix the bug is to directly express the design intent so that the class is correct by construction. Since the problem is that we are not expressing the “not null” invariant, we should express that by construction — one way is to make the pointer member a gsl::not_null<> (see for example the Microsoft GSL implementation) which is copyable but not movable or default-constructible. Then the class is both correct by construction and simple to write:

// Corrected class: Declare intent, naturally get only copy and not move

struct IndirectInt {
    not_null<shared_ptr<int>> sp = make_shared<int>(42);
public:
    // ... more functions, but NOT using defaulted move functions
    //     which are automatically suppressed
    bool operator<(const IndirectInt& rhs) const { return *sp < *rhs.sp; }  // ok
    // ...
};

IndirectInt i[2];
i[0] = move(i[1]); // performs a copy
sort(begin(i), end(i)); // ok, no undefined behavior

There’s one more question before we leave this example…

But what about a third option, that the class intends (and documents) that you just shouldn’t call operator< on a moved-from object… that’s a hard-to-use class, but that doesn’t necessarily make it a buggy class, does it?

Yes, in my view it does make it a buggy class that shouldn’t pass code review. The fundamental point is that “moved-from” really is just another ordinary state that can be encountered anytime, and so the suggested strategy would mean every user would have to test every object they ever encounter before they compare it… which is madness.

But let’s try it out: In this most generous view of IndirectInt, let’s say that the class tries to boldly document to its heroic users that they must never try to compare moved-from objects. That’s not enough, because users won’t always know if a given object they encounter is moved-from. For example:

void f(const IndirectInt& a, const IndirectInt& b) {
    if (a < b)  // this would be a bug without first testing (somehow) that a and b both aren't moved-from
       // ...
}

Worse, it can be viral: For example, if we compose this type in a class X { Y y; IndirectInt value; Z z; /* ... */ }; and then make a vector<X> and use standard algorithms on it, some X objects’ value members can contain null pointers if an exception is thrown, so there would have to be a way to test whether each object of such a composed type can be compared.

So the only documentable advice would be to require users of IndirectInt, and by default of every other type that composes an IndirectInt, to always test an object for a null data member in some way before trying to compare it. I view that as an unreasonable burden on users of this type, nearly impossible to use correctly in practice, and something that shouldn’t pass code review.

Note that even floating point types, which are notoriously hard to use because of their NaN and signed-zero mysteries, are generally not this hard to use: With IEEE 754 non-signaling relational comparison, they support comparing any floating point values without having to first test at every call site whether comparison can be called. (With IEEE 754 signaling relational comparison, they’re as hard to use as IndirectInt. See your C++ implementation’s documentation for which kind of floating point comparison it supports.)

Does the “moved-from” state correspond to the “partially formed but not well formed” described in Elements of Programming(aka EoP)?

Not quite.

In EoP, the description of an object’s state as “partially formed but not well formed” is similar to the C++ Standard’s description of “valid but unspecified.” The difference is that EoP requires such objects to be assignable and destroyable (i.e., partially formed) while the C++ standard makes a broader statement that “operations on the object behave as specified for its type” and that a moved-from object “must still meet the requirements of the library component that is using it.” (See Cpp17MoveConstructible and Cpp17MoveAssignable.)

Trip report: Winter ISO C++ standards meeting (Prague)

A few minutes ago, the ISO C++ committee completed its final meeting of C++20 in Prague, Czech Republic. Our host, Avast Software, arranged for spacious and high-quality facilities for our six-day meeting from Monday through Saturday. The extra space was welcome, because we had a new record of 252 attendees. We currently have 23 active subgroups, and met in nine parallel tracks all week long; some groups ran all week, and others ran for a few days or a part of a day, depending on their workloads.

See also the Reddit trip report, which was collaboratively edited by many committee members and has lots of excellent detail. You can find a brief summary of ISO procedures here.

ISO C++ committee in Prague, on the occasion of completing C++20 (February 2020)

C++20 is done!

Per our published C++20 schedule, we finished technical work on C++20 at this meeting. No features were added or removed, we just handled fit-and-finish issues including addressing all of the 378 national body comments we received in last summer’s international comment ballot (Committee Draft, or CD). The next step is that the final document will be sent out for its international approval ballot (Draft International Standard, or DIS) and will be published later this year.

In addition to C++20 work, we also had time to make progress on a number of post-C++20 proposals, including continued work on contracts, networking, executors, reflection, compile-time programming, pattern matching, and much more. We also discussed ABI stability and took polls that said we are definitely not willing to guarantee pure ABI stability forever, and we are ready to consider proposals (especially ones that enable performance improvements) even if they may require an ABI break or migration on some platforms for affected types and functions, but that we aren’t ready to take a broad ABI break across the entire standard library. This is an important and meaningful decision, and an engraved invitation for proposal authors to bring proposals (and to bring back previously rejected ones) for their “wish lists” of such potentially-affected features, as soon as our next meeting this June. I’m looking forward very much to seeing how this can spur further C++ standard library innovation for C++23.

Speaking of C++23…

C++23 schedule and priorities

For the first time, we formally adopted a schedule and a planned set of feature priorities for the next round of standardization, C++23, right at its outset.

The schedule for C++23 reaffirms that we’ll use the same meeting and feature-freeze deadline schedule that we used for C++20. Note that this means we are “only” two years away from the feature freeze of the next standard! Two years has a way of going by really quickly – “warning: objects in the schedule are closer than they appear.”

The priorities for C++23’s feature set are to focus our work on the following, emphasizing upgrades to the standard library:

  • “Finishing C++20” with standard library modules and library support for coroutines. This will let programmers use the standard library via  modules, and easily use coroutines with futures and other common types right “out of the box” (today some additional coding or a helper library is required).
  • Adding executors and the networking library that relies on executors.

On the language side, we will prioritize progressing the following as quickly as possible, for C++23 “if possible” but we’ll know better in a year or two whether they are likely to make it for 23 or not:

  • Reflection, including introspection to query the program, compile-time programming to manipulate the results, and generation to inject new entities into the program.[1]
  • Pattern matching, which also progressed at this meeting with continued feedback on proposals.
  • Contracts, which we spent another half-day on in SG21 on Friday afternoon.

As a second priority, the wording groups will also prioritize bug fixing higher than in the past, to pay down technical debt faster.

There will also be plenty of work on other features, so do expect C++23 to contain other work too. The purpose of setting these priorities is to mainly to say that at any given meeting we are not going to spend a lot of time working on other proposals until we have made as much progress as possible on the above ones first, that’s all. This way at each meeting we will give these proposals’ authors the maximum help and direction we can, so they can get as much further work done in the gap until the next meeting.

Finally, note that “priorities” doesn’t mean “commitments.” Prioritizing these features is not a commitment that they’ll all be done in time for C++23, though we hope that most of them may likely be. Watch the next two years’ trip reports and you’ll get a good sense of how we’re making progress against these priorities.

Wrapping up

Thank you again to the 252 experts who attended this final meeting of C++20, and the many more who participate in standardization through their national bodies!

But we’re not slowing down… in less than four months we’ll be meeting again in Varna, Bulgaria, for the first meeting to start adopting features for C++23. I look forward to seeing many of you there. Thank you again to them and to everyone reading this for your interest and support for C++ and its standardization.

Notes

[1] For those who are interested in my metaclasses proposal, this is ~98% of metaclasses – as soon as this reflection work lands, the only thing that will be left for me to propose to complete metaclasses is to add a declaration syntax like class(M) as “just” a minor syntactic sugar for invoking a consteval function that takes a meta::info reflection of the class body as input.

Last night's talk video is online: Quantifying C++'s accidental complexity, and what we really can do about it

The ISO C++ committee is here in Prague this week to finish C++20, and the meeting hosts Avast Software also arranged a great C++ meetup last night where over 300 people came out to see Bjarne Stroustrup, Tony Van Eerd, and me give talks. The videos are already online, see below — they’re really high quality, and it was a great event. Thank you again to everyone who came out to see us! You made us feel very welcome in your beautiful city and your enthusiasm for C++ is contagious (in a good way!).

Mine was a brand-new talk with material I’ve never presented on-camera before. (I gave a beta version at ACCU Autumn last November in Belfast.) I’m really excited about this upcoming work that I’m planning to bring to the committee in the near future, and I hope you enjoy it.

Thanks again to Hana Dusíková for her hard work organizing this meetup and this entire week-long record-shattering C++ standards meeting, by far the largest in history with 250 attendees. But there’s no rest for the competent — she still has to chair SG7 (reflection and compile-time programming) all day tomorrow. :) I’ll be there!