Skip to main content

What Comes to Mind

Stuff, and more stuff

✌Existing Practice✌

"Standardize existing practice" was barely true of the first C++ standard, and it is the wrong rule now. It was already being stretched in 1998. The ARM was existing practice. Templates as specified in the ARM were not what got standardized; the STL had two years of use at HP and a handful of early adopters before it was voted in; namespaces, exceptions as shipped, and iostreams with locales were designed in committee and implemented afterward. The result was a standard that no compiler implemented for years. We call it a success because C++ survived it, not because the process was sound.

Today the rule is not merely stretched. It is objectively wrong, for two reasons that are usually run together and should be kept apart.

No library can be brought in as-is

The ecosystem is too fragmented. There is no library that everyone uses, built the way everyone builds, with the error handling everyone wants, that could be dropped into namespace std untouched. And even if there were, LEWG would not do it. The correctness bar applied to current proposals guarantees significant changes to any library that comes through. Lifetime, constexpr, exception safety, allocator awareness, freestanding, ABI. A library with real users has made choices on all of those that the committee will unmake.

That is not a complaint. It is a description of what the process is for. The library that gets standardized is a new thing with the proposal as its ancestor.

Wide deployment experience is never coming back

For a language feature, deployment experience is not going to happen again. No one is going to commit experimental language code to a production code base. The risk of being wrong, of having to strip syntax out of a million lines when the design changes, is too high for any engineering organization to accept. For a vendor library extension it is unlikely for the same reason, with the added problem that vendors have learned that shipping something under std::experimental means supporting it forever.

This does not relieve a proposal of the obligation to implement. If anything it makes the implementation more important. But the purpose of the implementation has changed. It is there so that other people can gain intuition about the feature, and so that the implementer can confirm the lack of breakage. Those are the two things a reference implementation is for. Adoption numbers are not one of them.

How are we doing?

The evidence for this is the last three standards.

fmt is the closest thing we have to the old model. Victor Zverovich had a library with wide deployment, a settled API, and years of bug reports. std::format is still not fmt. Compile-time format string checking went in late through a separate paper. std::print waited for C++23. The escape and width rules for Unicode were rewritten in SG16. It is a success because the library kept living alongside the standard, absorbing the committee's changes and feeding back what broke. The deployment experience that mattered was the experience of tracking the delta.

Ranges is the same story with a larger delta. range-v3 was widely used. std::ranges diverges from it on borrowed ranges, on dangling, on the view semantics, and on what got left out. C++20 shipped without zip, enumerate, chunk, or to. C++23 filled the holes. That is a success on a six-year timeline, and it worked because Casey Carter and Eric Niebler treated range-v3 as the experiment, not the product.

Senders and receivers look like a success, and it is the case that best fits the new rule. There was no existing practice. P0443 Executors was an attempt to standardize existing practice, three existing practices in fact, and it failed because they could not be reconciled. P2300 started over. libunifex and stdexec were implementations built to let people gain intuition and find breakage. The design changed enormously between R0 and adoption, and it changed because people wrote code against the implementation and reported what was wrong. Nobody deployed it to production in the meantime. That was not the point.

Coroutines, before S/R, were marginal. The language feature shipped in C++20 with a working MSVC implementation and no library, and std::generator did not arrive until C++23. Everyone wrote their own task type and their own scheduler, which is what the design intended, and the result was that nobody's coroutine code interoperated with anyone else's. The feature only becomes useful now that S/R supplies the library half. The implementation existed. The intuition did not, because there was no shared thing to build intuition on.

Modules are not a success; they are the case that should have taught us. Two implementations existed, Microsoft's and Clang's, with different designs. The merged design that was voted into C++20 was implemented by nobody at the time of the vote. No build system understood it, and there was no way for anyone to gain intuition about how the merged thing behaved or to confirm it did not break existing code, because it did not exist. Six years later import std; works on some toolchains, header units are a partial implementation almost everywhere, and the amount of production C++ that has moved to named modules is a rounding error. This was a paper design standardized on the strength of experience with two things that were not it.

Contracts

Contracts are the current disaster. Pulled from C++20 in Cologne because the design was not settled, they came back as P2900, a minimal viable product, and were adopted into C++26. The final vote in Croydon was 114 to 12 with 3 abstaining, which by committee standards is a fight, and it is still not over. More than twenty national body comments came in against the design. A paper to defer to C++29 circulated three weeks before the vote. The opposition has not gone quiet since, and the DIS ballot is still open. Stroustrup's line is that it is not minimal and not viable.

The stated objection, when you strip the specifics about evaluation semantics and ignorability, is that there is not enough deployment experience. That is the impossible standard. We are never going to get deployment experience for a language feature that might change. There is a GCC implementation in trunk and a Clang branch. People have written code against them, found the sharp edges, and filed papers. The evaluation semantics debate happened because people could try the thing, which is the system working.

If Contracts derail C++26, or derail themselves into C++29, it will not be because the feature failed to meet a reasonable bar. It will be because the bar being demanded is one no language feature can meet, and one that Modules, with far less implementation behind it, was never held to.

Profiles

And then there are Profiles, targeting C++29, from many of the same people who gave us Modules. The pitch is safety, and the claimed existing practice is the Core Guidelines checkers. Those checkers are not Profiles. Profiles as proposed are a language mechanism with enforcement semantics that no compiler implements and no code base has been compiled against. It is the Modules shape exactly: a design with adjacent experience being presented as the experience.

The lesson from the history is not "demand deployment." It is "demand an implementation that other people can use to find out whether the design is wrong, and then listen when they do." fmt and ranges had that and survived heavy revision. S/R had that and was revised almost completely. Coroutines had half of it. Modules had none of it. Contracts have it and are being held to a standard they cannot meet, while Profiles are heading toward the standard Modules were held to, which was none.

Scrap Your static_assert

The obvious way to test a compile-time fact is static_assert. It's right there, it needs no framework, and for a fact that has to hold it's the right tool. As a test, though, it has one bad property: a wrong answer is a translation failure. The build stops at the first one, you get a compiler diagnostic instead of a test result, and every other test in the file goes unrun. The xUnit report is empty. You learn that something is wrong, once, and nothing about the rest.

There's a second, smaller problem. Even when you write the check as a runtime CHECK so that it gets reported, a bare trait doesn't report anything you can use:

CHECK(std::is_same_v<decltype(*e), int&>);  // FAILED: CHECK( false )

The expansion is the word false. You already knew the two types differed; the framework won't tell you what either of them was.

Converting the expected tests off static_assert came down to two header-only components that fix these two problems. Neither is clever. (The title owes Lämmel and Peyton Jones; the debt stops at the title.)

Type identity as a value

The fix for the second problem is to compare type identities that carry their spelling for diagnostics, instead of comparing a bool. See type_name.hpp. The comparison is still std::is_same_v, so the verdict is exact and a false pass isn't possible:

template <class T, class U>
constexpr bool beman::expected::testing::operator==(type_name_t<T>, type_name_t<U>) {
    return std::is_same_v<T, U>;
}

The spelling is consulted only after a comparison has already failed and the framework needs to explain it. So a failing check explains itself:

FAILED: CHECK( type_name<decltype(*e)>() == type_name<int&>() )
with expansion: const int& == int&

And the tests read like the trait they replaced:

TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") {
    using expected_t = expt::expected<int, int>;
    CHECK(type_name<decltype(*std::declval<expected_t&>())>() == type_name<int&>());
    CHECK(type_name<decltype(*std::declval<const expected_t&>())>() == type_name<const int&>());
    CHECK(type_name<decltype(*std::declval<expected_t&&>())>() == type_name<int&&>());
    CHECK(type_name<decltype(*std::declval<const expected_t&&>())>() == type_name<const int&&>());
}

Reporting a compile-time value at runtime

The fix for the first problem is to split the two questions a constexpr test actually asks. "Can this be constant-evaluated at all?" is a property of the code; it stays a hard translation failure, which is correct, because that's a fact that has to hold. "Does it produce the right answer?" is a property of a value, and there's no reason a wrong value should stop the build.

constant_eval.hpp is consteval, so a call to it is evaluated during translation. If the probe body isn't usable in a constant expression the program is ill-formed, and the first question is answered by the call itself, with no static_assert needed. The result then behaves as an ordinary prvalue, free to be handed to CHECK. The whole thing is a one-line wrapper:

template <class Probe>
consteval auto beman::expected::testing::constant_eval(Probe probe) {
    return probe();
}

A probe is a plain lambda that reduces what it observes to a literal aggregate:

TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") {
    constexpr auto probe = [] {
        constexpr expt::expected<int, int> e;
        return int_state{e.has_value(), *e};
    };
    CHECK(constant_eval(probe) == int_state{true, 0});
    CHECK(probe() == int_state{true, 0});
}

Because the probe is a plain lambda and not a consteval one, the same body runs in both evaluation modes. Constant evaluation and ordinary evaluation can take different paths through a union-based type like expected, so running both earns its second line:

CHECK(constant_eval(probe) == expect);  // constant evaluation
CHECK(probe() == expect);               // ordinary evaluation

Give the returned aggregate an operator<<. Without one, Catch2 prints {?} == {?} and you're back where static_assert left you.

What it buys

Two things. The reporting is better: a mismatch names both types, or prints both states, instead of expanding to false or stopping at a diagnostic before it can say anything. And a wrong answer is no longer a compile failure that blocks everything behind it. The suite builds, runs, and reports every case; a broken trait shows up as one red line among the green, with the rest of the run intact.

None of this abolishes static_assert. The genuinely ill-formed cases stay ill-formed, checked in their own negative-compilation files. What moved to runtime is only the part that was a test wearing an assertion's clothes.

An Infix Backtick Operator for C++

I am working on a proposal to let any callable be written between two backticks as an infix binary operator: x `f` y means exactly f(x, y). It is borrowed from Haskell, it desugars to an ordinary call, and I have it working in both Clang and GCC. This post is the announcement and the design tour.

Read more…

Refreshing a Stale Git Subtree

I write my WG21 papers with MPark/WG21, a Pandoc-based framework I vendor into the paper repo as a git subtree. The framework had a major overhaul–the build system split apart, and Pandoc jumped from 2.18 to 3.9–and my copy was 98 commits behind. Worse, the subtree had drifted in two directions at once: real local patches for a TLS-intercepting corporate network, and a pile of pointless autoformatter churn from my own pre-commit hooks. This is how I dragged it back to a verbatim copy of upstream, moved to the new flat.mk include, and pushed every local change back out of the subtree so the next update is a one-liner.

Read more…

Moving Forward With Legacy Encodings

1. Abstract

Reverse-parsing legacy multibyte text encodings — such as Shift_JIS, Big5, or GB18030 — using only local context is an unsolvable problem. Unlike UTF-8, which guarantees \(O(1)\) self-synchronization, legacy encodings have heavily overlapping lead and trail byte ranges. Consequently, even if you begin at a known, valid character boundary, computing the byte-width of the preceding character requires an \(O(N)\) backward scan to the beginning of the string to resolve the parity of the sequence.

The WHATWG decoding algorithms provide no mitigation, as their forward-looking state machines reset completely at every boundary. Robust reverse iteration through these encodings cannot be solved algorithmically in situ; it requires maintaining an external cache of boundary offsets established during a forward pass.

What follows is the story of attempting to find a way out, and why the math forces us to fail.

Read more…

Tailwind, Modus Themes, and the Blog Theming Workflow

I replaced the Foundation 6 theme on this blog with Tailwind CSS. The immediate motivation was a CSS conflict—Foundation's global code and kbd rules bled into org-mode source blocks—but the deeper reason is that Tailwind has the community and documentation that Foundation no longer does.

This post documents the workflow: how the theme is structured, how syntax highlighting CSS connects Emacs to the browser, and what the current configuration choices are.

Read more…

Surround With UUID

The question of why C++ is standardized through ISO comes up fairly often. This is what I came up with as an explanation the last time I tried to answer that.

It's a radically oversimplified too long elevator pitch.

Why Standard Organizations

The question of why C++ is standardized through ISO comes up fairly often. This is what I came up with as an explanation the last time I tried to answer that.

It's a radically oversimplified too long elevator pitch.

Read more…

The Sender Sub-Language

The paper The Sender Sub-Language by Vinnie Falco <vinnie.falco@gmail.com> and Mungo Gill <mungo.gill@me.com> makes extensive use of my work at https://github.com/steve-downey/sender-examples. The code is also the basis for my talk at C++Now 2023, Using the C++ Sender/Receiver Framework: Implement Control Flow for Async Processing. They present the code accurately and fairly, and I am very happy they found it useful in describing and understanding the capabilities of Senders in the framework. There is no higher praise than someone finding your work useful to build upon.

Nonetheless, we come to different overall conclusions about the Sender/Receiver framework.

Read more…

Building vcpkg dependencies with project toolchain

Making sure vcpkg delivers packages built with your toolchain is not hard, but much of the advice on the internet is flat wrong. You need to specify your toolchain both in your project and in the the vcpkg triplet. There's an airgap between your project and the dependency in vcpkg install. The CMake settings can't just flow through.

Read more…