Skip to main content

What Comes to Mind

Stuff, and more stuff

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…