March 17, 2023·5 min read·engineeringcppclangtemplates

Clang 16 lets wrapper types preserve trivial operations

Conditional special members reduce the machinery needed to preserve a wrapped type's properties, with documented limits around deleted functions.

A wrapper around a simple value should not need elaborate inheritance machinery merely to preserve that value's copying properties. Clang 16 implements conditionally trivial special member functions, allowing constraints to distinguish an ordinary defaulted operation from a more complicated operation needed by another instantiation.

The LLVM 16.0.0 release arrived on March 17. Its Clang notes list P0848 among newly implemented C++20 work. The notes also retain an explicit limitation, handling of deleted functions is not fully compliant because DR1496 and DR1734 are not implemented.

Templates are one of C++'s strongest arguments for me when an abstraction needs to preserve the properties of the data it wraps. The cost often appears in the implementation that makes the abstraction possible. This change interests me because it reduces that machinery while retaining a type-level distinction the optimizer and other library code can use.

A function that cannot apply should not decide triviality

Barry Revzin and Casey Carter's P0848R3 describes the problem through constrained copy constructors. One declaration is defaulted under a constraint. Another supplies a nontrivial implementation for cases requiring custom behavior.

Without the revised rules, merely having the additional nontrivial copy constructor can prevent the class from being trivially copyable. That remains true even for an instantiation where constraints make the defaulted constructor the relevant choice. The class's property then fails to follow the behavior available for that instantiation.

The proposal introduces eligibility into the definitions. Constraints must be satisfied, and a more constrained special member of the same kind can determine which operation is eligible. The type's triviality rules then consider the eligible operations.

This is more precise than asking whether one particular copy expression happens to select a trivial function. Triviality remains a property of the type. The paper's revision history explains that the committee wanted to avoid making the answer depend on an arbitrary expression context.

For a library author, this means constraints can describe both availability and the properties of the available operation. A defaulted special member can preserve the simple case. Another definition can handle a payload whose own operation requires more work.

Wrapping a value should preserve useful information

The paper uses optional-like storage as a motivation. A wrapper may need to track whether a value exists and construct it conditionally. For a simple payload, its copy operation can have different properties from the operation needed by a string-like payload.

The implementation challenge is to express both cases without forcing them to share the least favorable property. Earlier techniques can use layers of conditional base classes to produce the desired declarations. Those layers solve a real problem but make the source more difficult to read and maintain.

Constrained special members put the distinction beside the operation itself. The author can state when the defaulted declaration applies and when custom behavior applies. The resulting implementation can more closely resemble the operation's logical cases.

This does not mean every wrapper automatically becomes trivial. The wrapped type and the wrapper's other special members still matter. The facility supplies a way to express the relevant alternatives, not a blanket optimization request.

My preference is for the implementation to show why an operation has a property. Constraints attached to the special member make that relationship easier to inspect than a hierarchy whose main purpose is to arrange which declarations exist.

Structured bindings become easier to capture

The same release implements P1091R3 and P1381R1, allowing structured bindings to be captured in lambdas. This removes another case in which a local abstraction required surrounding source to work around a language restriction.

A pair supplies two names in this example. The lambda captures each binding by value and computes their sum.

#include <utility>

int combined()
{
    auto [left, right] = std::pair{3, 5};
    auto compute = [left, right] { return left + right; };
    return compute();
}

The example depends on the C++20 capture support documented for Clang 16. It demonstrates value capture of the bindings. It does not make a claim about every lifetime arrangement involving references into an external object.

This matters because structured bindings and lambdas are both common tools for keeping code local. An implementation gap between them can force a programmer to introduce replacement variables solely to move values into a closure. Supporting their composition lets the source retain the names introduced by decomposition.

The broader lesson for adoption is specific to implementation status. Both facilities belong to C++20 work in these release notes, even though the compiler also adds C++23 features. A compiler upgrade can improve support for an already selected language mode without requiring adoption of the newest standard mode.

Default mode and implemented facilities are separate

Clang 16 changes its default C++ mode from GNU C++14 to GNU C++17. A build that omits a language-mode option therefore changes its selected rules on upgrade. That default does not automatically select C++20 for the conditional-member or capture facilities discussed here.

The capture example can be saved in a source file. Its compiler command should select the intended mode explicitly.

clang++ -std=c++20 -fsyntax-only capture.cpp

The command records the language requirement. It does not require a particular standard-library implementation beyond the facilities used by this small example, and it does not establish support for every other C++20 feature.

The release notes also describe constraint-instantiation fixes and implementation of parenthesized aggregate initialization. These changes reinforce that conformance advances through individual rules and their interactions. The label C++20 does not reduce those interactions to one yes-or-no property.

The documented deleted-function limitation is therefore part of the conditional-triviality story. A library using unusual combinations of constrained and deleted special members needs the specific behavior described by its compiler, not just the headline that P0848 is implemented. Clang 16 supplies a more direct implementation path for ordinary constrained wrappers while explicitly identifying the remaining gap.

For a reusable wrapper, this also makes the supported instantiations part of its documented contract. The simple payload case and the custom-copy case can have different properties. Users of the wrapper need those differences to follow the payload consistently across compiler versions.