Trip Report: Winter ISO C++ Meeting in Hagenberg, Austria

Last week, I attended the winter 2025 meeting of the ISO C++ standardization committee in Hagenberg, Austria. This was the sixth meeting for the upcoming C++26 standard, where we finalized the feature freeze for C++26. Any features not yet approved by the language or library evolution working groups will not to be in C++26.

We added contracts, trivial relocation, and standard library hardening to C++26. Reflection is still pending wording review, but it will hopefully be added at the next meeting in June. Unfortunately, pattern matching did not make it into C++26.

As usual, you can find the full list of papers approved or discussed on Reddit. Here, I want to focus on contracts, profiles, and a problem with infinite ranges.

Contracts and standard library hardening

P2900 adds a minimal implementation of contracts to C++26—essentially a fancier version of the assert macro. P3471 then builds on top of it, standardizing the use of contracts to check a subset of safety-critical preconditions in the standard library, such as std::vector::operator[] (if you enable a so-called hardened implementation). This is a big step towards making C++ more memory safe: with a hardened standard library implementation, bugs like buffer overflows are less likely to lead to security vulnerabilities, and with contracts, you can add relevant preconditions to your own code, too.

However, there are two pitfalls you need to watch out for.

The first is related to the ability to turn contracts off. Concretely, contracts define four evaluation semantics:

  • ignore, which does not evaluate your contracts at all
  • observe, which checks your contracts, invokes a user-defined contract violation handler if a contract is violated, and continues execution
  • enforce, which checks your contracts, invokes a user-defined contract violation handler if a contract is violated, and terminates execution
  • quick_enforce, which checks your contracts and terminates execution if a contract is violated

quick_enforce is designed to have so little overhead that it can be enabled in production without a significant performance impact. It comes out of Apple's experience with shipping a hardened libc++, and Google found that it slowed down Chromium by only 0.3%. enforce is useful when you want to do custom logging and reporting of contract violations. observe is necessary to start adding contracts to an existing code base: if preconditions haven't been checked before, enforcing them will terminate for benign precondition violations like &vec[vec.size()]. With observe, you can collect all precondition violations into a log file and fix them one by one. Finally, ignore should only be used when a contract check is too expensive—for example, when checking that a range is sorted before calling std::lower_bound. Such a check can only be enabled in certain debug builds.

The problem with these semantics is that, right now, they are global: there is no standardized way to group different contracts into different categories and use different evaluation semantics for each category. It is likely that an implementation will allow you to specify different contract evaluation semantics for the hardened standard library preconditions, and Clang will probably add custom attributes to label contracts. However, with standardized C++26, a library developer cannot use contracts as a safety feature, that is, rely on guaranteed precondition checks to harden security-critical code. There are, of course, proposals to add this facility to C++29, but right now, you're out of luck.

The second pitfall happens when you link multiple translation units compiled with different contract evaluation semantics. Consider a header that defines an inline function with a contract abs: it has a precondition that x is not INT_MIN (as -INT_MIN is an integer overflow) and a postcondition that the result is nonnegative.

inline int abs(int x)
  pre (x != INT_MIN)
  post (result: result >= 0)
{
  return x < 0 ? -x : x;
}
header.h defines an inline function with a contract.

In fast.cpp, abs is used to crunch some numbers. For performance reasons, we have set the contract evaluation semantics to ignore.

#include "header.h"

int number_crunching(std::span<int const> data)
{
    return stdr::fold_left(data | stdv::transform(&abs), 0, std::plus<int>{});
}
fast.cpp uses abs and is compiled with contract checking disabled.

In safe.cpp, abs is used to process some user input. To avoid safety vulnerabilities, we have set the contract evaluation semantics to enforce.

#include "header.h"

class MyContainer
{
public:
    int size() const { … };

    Data operator[](int idx) const
        pre (0 <= idx && idx < size())
    {
        return data[idx];
    }
};

Data process_user_input(MyContainer const& container, int x)
{
    x = abs(x);
    return container[x];
}
safe.cpp uses abs and is compiled with contract checking enabled.

Now consider what happens if you link fast.cpp and safe.cpp into one executable. Each translation unit defines one copy of abs, once compiled with contract checking and once without (for the sake of argument, let's assume abs() isn't actually inlined into the call site). As abs is defined in a header file, and as P2900 goes out of its way to ensure that the two different definitions are not a violation of the one-definition rule, the linker is allowed to assume that both copies of abs are identical and will only include one in the binary. If the definition with ignore semantics is used, we no longer have contracts checking: if the user enters INT_MIN, we will have an integer overflow!

Even worse, while compiling safe.cpp, the compiler is allowed to assume that the postcondition of abs is true (after all, the program will be terminated otherwise). This means that, after inlining operator[], the compiler sees the following code:

Data process_user_input(MyContainer const& container, int x)
{
    x = abs(x);
    __assume(x >= 0); // post-condition of abs

    if (!(0 <= x && idx < container.size())) {
        contract_violation();
        std::terminate();
    }
    return container.data[x];
}
The optimizer assumes contracts are checked in safe.cpp and optimizes accordingly.

The optimizer can then see that the 0 <= x check in the precondition is redundant because it matches the postcondition of abs. Therefore, the optimizer will eliminate the check. However, if the linker then replaces the abs definition with an unchecked definition, we have just eliminated a safety check. A user input of INT_MIN will now lead to a buffer overflow!

This is terrifying.

Of course, the solution is simple: never link code compiled with different contract evaluation semantics (or different compiler flags in general). If mixing different contract evaluation semantics was not allowed, we would not have a problem: the compiler could tag each translation unit with the contract evaluation semantics, and then the linker can refuse to link translation units with different semantics. However, the standard defines this code to be valid, so that's not an option.

Implementations now have to be clever to handle such errors. For example, they could give each function an ABI tag, marking it with the contract evaluation semantic. That way, instead of two abs definitions, we have abs.ignore and abs.enforce. However, this approach does not help if the inline function merely calls a function with a different contract evaluation semantics but doesn't have contracts itself.

It is not clear to me (or to implementers) whether a generic solution is possible.

Profiles

The idea behind profiles is to make C++ more memory safe by adding static analysis and runtime checks. Opting in to a profile will ban certain unsafe features (for example, pointer arithmetic) and add runtime checks to others (for example, array index operations). We saw two proposals for profiles in Hagenberg.

The first proposal, P3589, adds a generic framework for profiles. The idea is that you enable a profile for one translation unit using a [[profiles::enforce(P)]] attribute, locally suppress a profile for a statement using [[profiles::suppress(P)]], and require that an imported module enforces a profile using [[profiles::require(P)]].

I liked this design. However, I expressed concern that it requires the use of modules. The per-translation-unit granularity makes it incompatible with header files: if you include a third-party header, it will also be checked with whatever profile(s) you have enabled, and if you're writing a header-only library, you cannot enable a profile in your header file. The room agreed with me, and we voted in favor of adding some mechanism to enable profiles in certain scopes only.

This meant that we could not forward the proposal to the core working group for inclusion in C++26.

The second proposal, P3081, adds both a framework and specific profiles. Although we already spent a lot of time over Christmas reviewing it in telecons, it still wasn't ready. Specific rules are still missing edge cases—for example, a rule banning reinterpret_cast allows conversion to std::byte* but not to unsigned char*, and another rule regarding casts does not consider how C style casts can be used to cast to private base classes. Other rules haven't been thought completely through—for example, banning pointer arithmetic might also ban std::vector::iterator, while banning array-to-pointer decay makes it almost impossible to use string literals.

And those are just the issues people pointed out while reading the proposal! It lacks any implementation, so who knows what other issues will come up when it is actually enabled on real code. The paper simply wasn't ready for C++26, so instead we voted to further develop it into a white paper, where it can collect implementation experience and further feedback before being merged into the actual standard.

Infinite ranges

As described in the last trip report, I volunteered to write a paper that proposes a concept to detect infinite ranges, P3555. During review, we discovered that infinite ranges are not actually allowed!

10 - A sentinel s is called reachable from an iterator i if and only if there is a finite sequence of applications of the expression ++i that makes i == s. If s is reachable from i, [i, s) denotes a valid range.

11 - [...]

12 - The result of the application of library functions to invalid ranges is undefined.

[iterator.requirements.general]/10-12

views::repeat(0), which always results in 0 forever, is not a valid range: you cannot reach the sentinel by incrementing the iterator a finite amount of times. In fact, its operator== will never return true. This makes code like views::repeat(0) | views::take(10) undefined behavior, as it applies the library function operator| (and eventually views::take) to an invalid range. Technically, for (auto x : views::repeat(0)) is also undefined behavior (it calls the library function .begin()), as is views::repeat(0); (it calls the library function ~repeat_view()).

Given that the standard library provides infinite ranges, it would be nice to use them without immediately causing undefined behavior. SG9 voted to do something to fix that.

However, fixing it is nontrivial.

We don't want to simply drop paragraph 10. Something like std::subrange(ptr, ptr - 1) should still be undefined behavior, as [ptr, ptr - 1) is not a valid range. So, we need to extend the definition of a valid range to allow infinite ranges—for example, by saying a range is valid if s is reachable from i, or if you can do ++i as often as you want without reaching s or invoking undefined behavior in the process. This allows views::repeat(0) while still preventing std::subrange(ptr, ptr - 1).

However, this still would not allow views::iota(0), which generates the increasing integers 0, 1, 2, etc. Eventually, incrementing leads to signed integer overflow, which is undefined behavior. So, the definition would need to be tweaked to allow for that, maybe by introducing something like unbounded ranges, where you can increment until reaching undefined behavior. Alas, that would allow std::subrange(ptr, ptr - 1) again.

Once we have infinite ranges, we also need to rethink their difference type. We don't want the difference type to be an arbitrary precision integer for performance reasons, but if the difference type is finite, we cannot express all possible differences. But maybe that's fine because on 32-bit systems, you already cannot express the difference between two pointers more than 2 GB apart. So, there is precedent that not all differences can be expressed in the difference type. We just should make that explicit. This then requires a careful audit of all standard library algorithms that are specified in terms of ranges::distance(begin, end)—many applications of a function call to account for or prohibit infinite ranges as input.

Then there is views::iota(unsigned(0)), which is a true infinite range, as unsigned integer overflow is defined to wrap. However, what is the distance between an iterator pointing to 3 and an iterator pointing to 5? Is it really 2? Or maybe UINT_MAX + 2? We probably need some wording about the minimum distance between iterators.

Furthermore, there is the question of std::istream_view. When reading a regular file, it is finite, but when reading /dev/null or a pipe, it can be infinite. Is it really an infinite range if it == end can potentially be true, the specific program execution just never reaches that point? After all, it == end in that context is a non-deterministic question to ask.

Needless to say, there are a lot of questions SG9 has to resolve over the next year(s). And in the end, people will write the same exact code as they did before, just with a (hopefully) warm fuzzy feeling that it is now well-defined behavior.

— by Jonathan Müller

Do you have feedback? Send us a message at devblog@think-cell.com !

Sign up for blog updates

Don't miss out on new posts! Sign up to receive a notification whenever we publish a new article.

Just submit your email address below. Be assured that we will not forward your email address to any third party.

Please refer to our privacy policy on how we protect your personal data.

Share