Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,590 words · 1 segments analyzed
2026-10-11 This is a direct follow-up to my previous article, “Break MSVC and Clang with this one weird trick!”. In that article, I showed a solution for the problem in MSVC, but the problem in Clang seemed a lot more involved. As of the time of writing, the problem in Clang still hasn’t been fixed, although it looks like it’s close to being fixed. I foresee it being fixed soon. The problem is, I’m trying to support Clang 16 and onward, so I have to make do with what we have. This article will explain how to make this all work in Clang, and how I got there. A “backdoor pilot” conference trip report Similarly to how TV shows sometimes insert “backdoor pilots” into their episodes, I’m doing the same here. —–BEGIN TANGENT—– I don’t have enough important things to say about CppCon and NDC TechTown to warrant a full article, but I had a great time at both, and I have a couple particular things I wanted to mention here, among my few-times-per-year-typing-into-the-void that is my blog posts. I spoke at both CppCon 2026 and NDC TechTown 2026, about GDB pretty-printers. I got the same talk accepted at both conferences, so the talks had the same title and abstract, but I wanted to make the contents different. In the end, I made one very long slide deck that I split into 2 parts. Both talks had the same first ~25 minutes, and then the rest of each talk was unique. My main point remains the same, that we always need to be testing the code that we write, and that includes pretty-printers. I’ll write another article about my GDB pretty-printer unit testing library, that I spoke about in my talks. It was an honour and a pleasure to be able to speak at both of these conferences. For the first time ever at CppCon we decided to fill the exhibitor tables with community groups on the Thursday and Friday, when many of the exhibitors tend to leave. There’s still more to be done in the coming years, but it made the halls feel much more alive. One of the tables was for WG21, where we had a physical presence so anyone could walk by and ask questions face-to-face. It was lovely to speak with people! Hopefully next year we’ll have even more community exhibitors. If you’re reading this post and you want to see a table representing a particular community organization on the Thursday and Friday of CppCon next year, get in touch with me. The biggest highlight of CppCon for me was a particular phrase that Ben Deane accidentally coined on the spot. I can’t remember the exact details of the conversation, but we were discussing the idea of “standing on the shoulders of giants”, and the strange process of actually becoming those aforementioned “giants”. Then he said, (sharing this quote with permission), Take your place in the tower of giants. I immediately froze, looked him straight in the eye and said, “Are you quoting something?”, to which he said “No, I just said that”, to which I said, “That’s incredible, I need to write this down”. And I did, which is how I know what he said verbatim. So this is me sharing that amazing coinage with the world. I have thought about this nearly every day since it happened a few weeks ago. The main reason I’m even writing this section of this article is because of this amazing quote. NDC TechTown was a lot of fun. Much shorter than CppCon, but much less work for me as just a regular speaker-and-attendee. The sense of community at NDC TechTown is incredible. I hope to get the chance to attend again. Anders Knatten’s live edition of CppQuiz was quite fun, but way more difficult than I had anticipated! Who knew C++ was hard. —–END TANGENT—– Where we left off In the last article, I first showed this code, which failed in both MSVC and Clang. struct S{}; template <class T> void foo(T) { (void)[]<class U>(U) consteval -> bool { return requires { 0 * T{}; }; }(0); } void bar() { foo(S{}); } In Clang, I reduced it down to this minimal repro, which still showed the same issue. I opened llvm/llvm-project#205218, and it turns out to be a duplicate of llvm/llvm-project#35052. template <class T = void> void foo() { [](auto) consteval -> void { }(0); } int main() { foo(); } This was worrying, because it means that generic lambdas cannot be evaluated at compile-time. I think sometimes they can be, and I was fortunate enough to only encounter those cases previously, but now I’m running into this bug. Because I’m writing a unit testing library focused around compile-time testing, the inability to evaluate generic lambdas at compile-time could kill the project, at least with the API I want. Why do we need generic lambdas? I have this macro, K3_K3TCHUP_EXEC_PACKET_(PACKET). This is a macro that’s internal to the library, and it must handle “packet” execution. In k3tchup, a “packet” is the name that I give to an invocable that encapsulates further test logic. Originally, I needed each packet to just be nullary invocable, so PACKET() would need to be a valid expression. However, now I have a 2nd kind of packet, which is invocable with an lvalue opaque “state” object. I need both of these packets to be handled, and I want them to be handled from the same macro. I can’t do dispatching in the preprocessor, as invocability is a semantic requirement, much after the preprocessor. Simplifying some identifiers, this is what I want the macro to look like. Note that K3_K3TCHUP_EXEC_PACKET_STATEFUL_ is a macro to contain some more non-trivial logic. #define K3_K3TCHUP_EXEC_PACKET_(PACKET) \ { \ using T = decltype(PACKET); \ if constexpr (std::invocable<T>) { \ (PACKET)(); \ } else if constexpr (std::invocable<T, state&>) { \ K3_K3TCHUP_EXEC_PACKET_STATEFUL_(PACKET); \ } else { \ static_assert(false); \ } } This looks great. What’s the problem? Don’t read ahead if you want to figure it out for yourself. … The problem is that this isn’t a template context. Or at least, it might not be a template context. Imagine this calling scenario, which is about as simple as it gets. void foo() { K3_K3TCHUP_EXEC_PACKET_([](state&) {}); } If we do just 1 round of expanding the macro, this function becomes: void foo() { { using T = decltype([](state&) {}); if constexpr (std::invocable<T>) { ([](state&) {})(); } else if constexpr (std::invocable<T, state&>) { K3_K3TCHUP_EXEC_PACKET_STATEFUL_([](state&) {}); } else { static_assert(false); } }; } But because this isn’t a template, the if constexpr statements don’t work as we might expect. There is nothing that’s dependent on a template parameter, so the compiler is free to evaluate the constants when encountering them. Ultimately the compiler sees this: void foo() { { if constexpr (false) { ([](state&) {})(); } else if constexpr (true) { K3_K3TCHUP_EXEC_PACKET_STATEFUL_([](state&) {}); } else { static_assert(false); } }; } Outside of a template context, if constexpr doesn’t work the same way. Using if constexpr (false) doesn’t stop the enclosed statements from being semantically checked. So in this case, the compiler runs semantic analysis on ([](state&) {})();, and we get a hard error. Not to mention the static_assert(false); which would also net an error. I need some way to do this dispatching, and if constexpr is the tool in my tool belt. So let’s just wrap this whole thing inside an immediately invoked lambda, and make sure the lambda is generic so we can depend on the template parameter. #define K3_K3TCHUP_EXEC_PACKET_(PACKET) \ [&]<class T>(std::type_identity<T>) { \ if constexpr (std::invocable<T>) { \ (PACKET)(); \ } else if constexpr (std::invocable<T, state&>) { \ K3_K3TCHUP_EXEC_PACKET_STATEFUL_(PACKET); \ } else { \ static_assert(false); \ } \ }(std::type_identity<decltype(PACKET)>{}) Here, the compiler won’t eagerly evaluate the statements that were previously eagerly evaluated. Even though this lambda is called exactly one time, and we can see with our eyes exactly what will happen, the compiler does not do the same thing as before. Therefore, after all this, it seems that we need to use generic lambdas here. Alright, but why do we need generic lambdas executed at compile-time? Previously, in my library, the nullary invocable packet was itself only ever executed at run-time. Even though within the packet, you could have many statements executing at compile-time. Here is a code example. EXPECT_THAT([]{ EXPECT_COMPILE_TIME(1 == 2); }); In this case, the expression 1 == 2 is only invoked at compile-time, but the lambda itself is only invoked at run-time. Because the lambda is only invoked at run-time, any template machinery that’s encapsulated in the EXPECT_THAT macro also probably involves running things at run-time. So even if I wrapped the logic in an immediately-invoked generic lambda, it would only execute at run-time. With the new type of packet, for “stateful” checks, I’ll need to execute the packet twice: once at compile-time and once at run-time. With that change, we can have some nicer syntax that looks like this. EXPECT_THAT([](auto& state) { std::optional<int> o = foo(); ASSERT(state, o.has_value()) << "optional did not have a value"; EXPECT(state, *o == 5) << "optional's value was not 5"; }); Because the packet itself (i.e. the lambda) must be executed once at compile-time and once at run-time, then whatever template machinery is encapsulating it would also need to have statements executing both at compile-time and run-time. Without these “stateful” checks, the best we could do is this: EXPECT_THAT([]() { ASSERT(foo().has_value()) << "optional did not have a value"; EXPECT(*foo() == 5) << "optional's value was not 5"; });