September 19, 2023·5 min read·engineeringcppclangtemplatesdiagnostics

Clang 17 delays template static assertions until they matter

CWG2518 lets a template use static_assert(false) for an unsupported case without rejecting the template before that case is instantiated.

An unsupported template case should fail when a program asks for that case. Earlier rules made the obvious way to express that failure reject the template before a caller ever selected it. Clang 17 implements CWG2518, allowing static_assert(false) in a template definition to wait until the relevant instantiation.

The LLVM 17.0.1 release was published on September 19. Its Clang release notes list the static-assert correction alongside other C++ conformance and code-generation changes. GitHub's release page identifies 17.0.1 as the version to use after withdrawal of 17.0.0.

I like templates when their abstractions preserve runtime efficiency, but the diagnostic rules can make simple intent hard to express. This correction removes a workaround that existed primarily to postpone an error. The source can state that a particular branch is unsupported at the branch itself.

Exhaustive compile-time selection needs a failure branch

Barry Revzin's P2593R1 motivates the change by comparing runtime and compile-time dispatch. A runtime chain can finish with an assertion for a supposedly impossible case. A similar if constexpr chain naturally ends with a static assertion.

Under the earlier template-validity rule, a branch containing an unconditional false assertion had no valid specialization. That could make the program ill-formed even when no instantiation selected the branch. The compiler could reject an unsupported case before knowing whether the application used it.

The revised behavior makes a simple implementation possible:

#include <type_traits>

template<class T>
constexpr int classify()
{
    if constexpr (std::is_same_v<T, int>) {
        return 1;
    } else {
        static_assert(false, "unsupported classification type");
    }
}

static_assert(classify<int>() == 1);

The function is declared constexpr so the supported instantiation can participate in the final compile-time check. Its selected branch returns an integer. The discarded branch retains the explicit failure for unsupported types, and requesting one of those instantiations produces the diagnostic.

The important language change concerns the assertion inside the template. It does not make an ordinary non-template static_assert(false) succeed. It also does not make an unsupported instantiation valid. The failure is deferred to the context that actually requests it.

Dependent false encoded timing indirectly

P2593R1 describes the established workaround of making the condition depend on a template parameter. An always-false variable template can produce the desired delay while allowing the compiler to treat the condition as dependent.

That workaround requires the reader to understand two intentions. The Boolean result must be false, and the expression must appear dependent so that checking happens later. The second requirement contributes no application meaning, but earlier rules made it necessary.

Other proposed workarounds use expressions such as a type's size compared with zero. The paper warns that some of these still violate the underlying rule even when a compiler happens not to diagnose them. Obscuring the false condition is not the same as giving it valid semantics.

The correction addresses the actual timing requirement. A static assertion in a template definition can remain present until the relevant instantiation. The programmer no longer needs to manufacture an unrelated property of the template argument just to postpone the error.

For me, that is an improvement in the readability of generic code. An unsupported branch can say so directly. A helper named dependent false should not be necessary for understanding why a dispatch function rejects a particular type.

Primary templates benefit as well

The paper also discusses primary templates intended to be specialized. A primary definition may exist only to produce a useful error when no supported specialization matches. An unconditional assertion expresses that requirement naturally.

The distinction is between defining the fallback and instantiating it. A library needs to make the fallback available so that a later unsupported use has a diagnostic. Rejecting the definition immediately prevents that arrangement from serving its purpose.

This is especially useful when the error message can name the missing support. A tailored assertion is closer to the library author's intent than a chain of failures caused by missing members inside an incomplete fallback.

The correction does not remove the need for constraints elsewhere. Constraints can select which overload participates or describe an operation's requirements. A deliberate assertion can instead identify a case that reaches a definition but has no supported implementation. Those are different positions in template processing.

The diagnostic's timing also determines who sees it. A header can define a generic facility without forcing every translation unit to instantiate its unsupported path. A caller that requests that path then receives the library's explanation in the instantiation context. This is useful for independently maintained libraries because including their declarations and using a particular specialization are separate actions.

A good assertion message should therefore describe the unsupported requirement. The revised language rule supplies the delay, so the message can concentrate on the library's actual limitation. That makes the definition and the eventual failure easier to relate without requiring the caller to understand an auxiliary dependency trick.

Other release changes have narrower conditions

Clang 17 improves unoptimized code generation for std::forward_like by treating it as a compiler builtin, similarly to familiar forwarding utilities. That is a code-generation change, separate from the static-assert language correction.

The release also improves dynamic_cast to final types by comparing vtable pointers where the ABI guarantees a unique vtable. The notes explicitly exclude the Microsoft C++ ABI from that optimization at this stage and provide a flag to disable the unique-vtable assumption.

These qualifications matter when reading a release summary. A language correction affects which source is accepted. A code-generation optimization can depend on the target ABI. Neither justifies a general claim that every C++ operation becomes faster or every template diagnostic becomes simple.

Clang also adds -std=c++23 as an alias for its existing draft-mode spelling and introduces experimental C++2c modes. The existence of those modes does not imply complete support for every facility associated with the edition. The notes explicitly mark some constexpr work as partial.

The static-assert correction is therefore best described individually. Clang 17 accepts the template definition and delays the false assertion until the corresponding case is instantiated. A library can use that behavior to state an unsupported branch directly, with the error attached to the use that makes the branch relevant.