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

Exercising Ranges (part 3)

1 July, 20151 July, 2015

(Start at the beginning of the series if you want more context.) So, I was going to implement monoidal_zip, and to do that, I would clone zip_with.hpp. So I did that. Eric’s a great library author, and the ranges code is pretty easy to read. For the most part I…

Read More

A persistent myth about STL’s remove (and friends)

8 March, 201530 June, 2015

There seems to be a persistent myth about STL’s remove, remove_if, etc. Ask even a relatively experienced C++ programmer to explain this code. vector v = { 1,2,3,4,5 }; v.erase(remove_if(v.begin(), v.end(), [] (int i) { return (i & 1) == 0; }), v.end()); They’ll recognize the erase-remove idiom and correctly…

Read More

Floating-point maths, constexpr style

13 October, 201515 October, 2015

(Start at the beginning of the series – and all the source can be found in my github repo) To ease into constexpr programming I decided to tackle some floating-point maths functions. Disclaimer: I’m not a mathematician and this code has not been rigorously tested for numeric stability or convergence…

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