Trip Report: Winter ISO C++ Standards Meeting in Tokyo

Ville wins the Sankel Award, a new [[nodiscard]] policy, and more.

David Sankel
Adobe Tech Blog

--

Another meeting, another slew of potential changes to standard C++. In this recap, I’ll summarize the working draft’s most significant changes, spotlight my favorite proposal at the meeting, Member customization points for Senders and Receivers, and discuss a handful of notable developments.

I want to point out some other Tokyo trip reports for those wishing to see different perspectives. If you’d like a detailed list of the outcomes of every paper, look at Inbal Levi’s Reddit write-up. See Herb Sutter’s report for a higher-level view of big-ticket item progress. If you’d rather see a blow-by-blow experience report, Jonathan Müller’s take will be of interest.

What’s new in the draft standard?

This snippet summarizes the notable changes:

// wg21.link/p2573r2
void newapi();
void oldapi() = delete(“oldapi() is outdated, use newapi() instead”);

void f() {
std::println(); // Shorthand for ‘std::println(“”)’. wg21.link/p3142r0

// Paths can be printed/formatted now. wg21.link/p2845r8
std::println(“Here’s a path: {}”,
std::filesystem::path(“/stlab/chains”));

std::vector<int> x{1, 2, 3};
std::array<int,3> y{4, 5, 6};


// Outputs 1, 2, 3, 4, 5, and 6 separated by newlines.
for( auto i : std::views::concat(x, y) ) // concat is new from
std::cout << i << std::endl; // wg21.link/p2542r8
}

The other changes approved at the meeting are either bug fixes or on the more niche side.

Sankel award winner: Member customization points for Senders and Receivers

The committee is betting big that Sender/Receiver, a concurrent/asynchronous execution proposal, will gain traction in the larger C++ ecosystem. Unfortunately, the framework’s complication is a hurdle even for experts. Many attempts to teach the system haven’t demonstrated its practical use, which is also a concern.

Consider retry, an example algorithm in the Sender/Receiver repository. retry(x) takes in a sender x and returns a sender that a) performs x’s operation repeatedly until it succeeds, and then b) transmits the resulting value to its connected receiver. The full implementation of this algorithm requires 131 lines of code, but let’s focus on retry’s return type, _retry_sender.

template <class S>
struct _retry_sender {

template <class Env>
friend auto tag_invoke(stdexec::get_completion_signatures_t,
const _retry_sender&, Env) -> /*blah*/;

template <stdexec::receiver R>
friend auto tag_invoke(stdexec::connect_t,
_retry_sender&& self,
R r) -> /*blah*/;

friend auto tag_invoke(stdexec::get_env_t,
const _retry_sender& self) -> /*blah*/;
};

This sender has three operations implemented by tag_invoke overloads:

  • connect joins the sender (value producer) to a receiver (value consumer)
  • get_completion_signatures communicates the value and error types the sender produces
  • get_env returns an object that describes various sender characteristics

Why are these operations wrapped in a tag_invoke function that uses what appears to be a dispatcher type as the first argument? This wrapping adds a lot of complexity.

Ville Voutilainen’s Member customization points for Senders and Receivers paper argued that these should be simple member functions. Note the difference:

template <class S>
struct _retry_sender {

template <class Env>
auto get_completion_signatures(const _retry_sender&, Env) -> /*blah*/;

template <stdexec::receiver R>
auto connect(_retry_sender&& self, R r) -> /*blah*/;

auto get_env(const _retry_sender& self) -> /*blah*/;
};

Not only is this much less complex, it resembles run-of-the-mill C++ code. Although many concerns about unnecessary complexity in Sender/Receiver remain, this is the first paper that successfully achieved a major simplification. For this accomplishment, Ville gets the Sankel award for best paper at the Tokyo C++ Standardization meeting.

Other developments

[[nodiscard]] policy

LEWG concocted a process for instituting policies to avoid rehashing arguments ad nauseam, such as when noexcept is appropriate. [[nodiscard]] placement is one of those recurring topics. Darius Neațu and I wrote a paper suggesting placement guidelines that balance verbosity and utility. Jonathan Wakely wrote another paper advocating for more liberal placement using general implementor recommendations instead of per-function guidelines.

The ensuing discussion revealed that implementers should make their own decisions about warning mechanics. Moreover, [[nodiscard]] specifications in the standard library, whether general recommendations or specific uses, are inconsequential. An implementer, for example, is free to warn on discarded operator== usages without [[nodiscard]] annotations.

A consensus emerged that [[nodiscard]] need not be used or mentioned in the standard library at all. The new policy ultimately became “don’t use it”:

[[nodiscard]] policy: Library wording should not use [[nodiscard]].

Rationale: Warnings are non-normative so [[nodiscard]] placement has a spurious impact. Furthermore, implementors are better qualified to identify when and where to emit warnings for their implementations.

Mission accomplished! Adios [[nodiscard]] discussions in LEWG.

You might ask, “How did the noexcept policy discussions go?” Let’s just say that they weren’t as productive as the [[nodiscard]] discussions.

std::optional range support

Marco Foco, Darius Neațu, Barry Revzin, and I proposed giving std::optional range support, which essentially allows treating std::optional objects as ranges of 0 or 1 elements. The immediate benefit is simplifying algorithms involving a range of optionals.

// A person's attributes (e.g., eye color). All attributes are optional.
class Person {
/* ... */
public:
optional<string> eye_color() const;
};

vector<Person> people = ...;

// Compute eye colors of 'people'.
vector<string> eye_colors = people
| views::transform(&Person::eye_color)
| views::join // works because optional is now a range.
| ranges::to<set>()
| ranges::to<vector>();

While this is nice to have, the paper’s more significant motivation was averting the direction of Steve Downey’s A view of 0 or 1 elements: views::maybe, which introduced a new, but subtly different, std::optional type called std::views::maybe with range functionality. The last thing C++ developers need is confusion about which std::optional type to use. Fortunately, LEWG saw the light and approved our paper.

Allocator-aware inplace_vector

In a surprise decision, Pablo Halpern’s paper attempting to add allocator support to inplace_vector lacked consensus for further work in LEWG. This outcome is encouraging. Allocator support for new datatypes is a considerable drain on committee resources, has error-prone usage, and is rarely used in practice. I’m happy to see decisions prioritizing features that benefit most C++ engineers over niche special interest groups.

std::print getting faster

std::print operates by formatting into a buffer and, in a subsequent step, writing that buffer to the output stream. The basic idea behind Victor Zverovich’s Permit an efficient implementation of std::print is to lock the stream and format directly into it to avoid the buffer overhead.

This approach has a problem, however. If in the process of formatting a string there is another call to std::print, a deadlock ensues. Through a series of polls, LEWG decided that the default formatter behavior should avoid this pitfall, but formatters known not to do this should be able to opt-in to the performance optimization. That led to the question of what the opt-in should look like.

After much discussion on the drawbacks of various options, Elias Kosunen proposed an opt-in strategy used in both Ranges and Sender/Receiver. The basic idea is to have a bool variable template defaulting to false:

namespace std {
template<class T>
constexpr bool enable_nonlocking_formatter_optimization = false;
}

Specialize it to opt-in to the optimization.

class MyType { /*...*/ };

// Opt-in to non-locking formatter optimization
template<>
constexpr bool std::enable_nonlocking_formatter_optimization<MyType> =true;

// MyType’s formatter
template <>
struct std::formatter<MyType> { /*...*/ };

This approach is clean and has the side benefit that those using inheritance to create formatters (please don’t do this) won’t accidentally get the optimization without an explicit declaration. This paper is an excellent example of designs substantially improving with LEWG expert feedback.

Conclusion

The Winter ISO C++ Standards Meeting in Tokyo made significant progress on proposals like Sender/Receiver simplifications and std::optional range support. Additionally, a new policy discourages [[nodiscard]] usage in the standard library, and we have a new performance opt-in for std::print. Overall, the meeting addressed many issues that continue to shape the future of C++.

Stay tuned for further developments in the upcoming meetings!

--

--