Skip to content
Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

Formatted Diagnostics with C++20

elbeno, 10 December, 202410 December, 2024

C++26 adds formatted diagnostics with static_assert, something like this:

static_assert(
  sizeof(int) == 4,
  std::format("Expected 4, got {}", 
              sizeof(int)));

The benefit of course is that when such an assertion fails, the compiler outputs a diagnostic that we control. But can this be done in C++20? Well, this is C++! So the answer is a qualified yes – we can get the compiler to output our formatted text.

We’re going to use a combination of techniques and serendipities to get there. And yes, macros are involved. Such is life. Here we go.

First: of course we can do string formatting in a constexpr context. I’m using fmtlib for this.

Second: we’ll use a C++20 structural-string type for a template argument. Most implementations are pretty much the same.

template <std::size_t N>
struct cx_string {
  // some constructors etc...

  std::array<char, N> value{};
};

template <cx_string S>
auto f() {
  // S is a compile-time string, yay
}

Third: in order to preserve the constexpr nature of our format function arguments (which are naturally known at compile time), we can use the wrap-in-a-lambda trick.

auto f(auto l) {
  // f is not necessarily marked constexpr
  // l cannot be marked constexpr
  // but we can get a constexpr value out
  constexpr auto value = l();
}

// lambda's call operator is constexpr
// and 42 is a compile-time value
f([] { return 42; });

Fourth: it is often convenient to print type and enumeration names at compile time, so we can use the well-known __PRETTY_FUNCTION__ trick to turn them into string_views at compile time.

Fifth: it’s going to be convenient to treat types and values the same, so we’ll use the previously-known trick for that, and we’ll combine it with the constexpr-preserving wrap-in-a-lambda trick.

// treat all of these the same, for formatting
CX_VALUE(42);
CX_VALUE(int);
CX_VALUE("Hello world"sv);

Sixth: we’ll use the happy fact that compiler diagnostics print compile-time strings out for us.

template <cx_string>
struct undef;

undef<"Hello, world!"> q{};
error: implicit instantiation of undefined template 'undef<{{"Hello, world!"}}>'

This works since clang 15 and GCC 13.2 AFAIK, but this is where we leave MSVC, which is still printing ASCII codes, behind. Also, clang has another problem:

undef<"Hello, world! this is a string that is longer than the compiler likes to print"> q{};
error: implicit instantiation of undefined template 'diag<cx_string<79>{{"hello, world! this is a string tha[...]"}}>'

Clang elides the string in the diagnostic after a certain size. It even does this when we pass -fno-elide-type (which is arguably a bug). So we’ll have to find another trick.

Seventh: somewhere in recent history, concepts were billed as an improvement to diagnostics. Which means compilers like to output concept check failures verbosely.

template <cx_string S>
concept check = false;

static_assert(check<"Hello, world! this is a string that is long">);
error: static assertion failed
...
note: because cx_string<44>{{"Hello, world! this is a string that is long"}} does not satisfy 'check'

So when we use a concept check failure, we get the whole message. (Thanks to Patrick Roberts for making me aware of this last piece of the puzzle.)

We’re done. All that remains is to put all these things together with about as reasonable an interface as we can manage, in C++20.

template <typename T> constexpr auto f() {
    STATIC_ASSERT(
      std::is_unsigned_v<T>,
      "hello {} {} {}", 
      CX_VALUE("world"), CX_VALUE(T), 123);
}

auto main() -> int { f<int>(); }
...
note: because 'check<cx_string<20>{{"hello world int 123"}}>' evaluated to false

There you have it: with a couple of the major compilers at least, we have user-formatted text in compiler diagnostics with C++20. This is C++. Putting a bunch of tricks under the hood in order to have a nice interface experience is what we do.

C++

Post navigation

Previous post
Next post

Related Posts

C++23’s new function syntax

5 September, 20225 September, 2022

We’ve had a couple of ways to spell functions for a long time: And we’ve had a shortcut for that second one for a while, too: (Aside: notice where [[nodiscard]] is positioned, since C++23’s P2173, to apply to the lambda’s call operator.) All these ways to spell functions look the…

Read More

C++ Tuples: the missing functionality

6 April, 201530 June, 2015

C++ provides a strange mix of compile-time and runtime functionality for dealing with tuples. There are some interesting parts, like std::tie to destructure a tuple, and std::tuple_cat to join together several tuples into one. So there is evidence that the standard has been influenced by some functional programming ideas, but…

Read More

Compile-time counters, revisited

14 October, 201515 October, 2015

(Start at the beginning of the series – and all the source can be found in my github repo) Some time ago I read a blog post showing how to make a compile-time counter: a constexpr function that would return monotonically increasing integers. When I first read it I didn’t…

Read More
©2026 Why is a raven like a writing desk? | WordPress Theme by SuperbThemes