Why C++26 Contracts might not work for all

C++26 contains contracts, a much-discussed feature. They are useful, but they are not the best choice for every use case.

When C++26 Contracts might not be the right tool

Why might a project decide not to use C++26 contracts?

Here are three points that need to be considered.

They may have no runtime effect

The first point is simple. C++26 contracts have ignore, observe, enforce, and quick_enforce evaluation semantics.

With the ignore semantic, a contract assertion has no runtime checking effect. With observe, a violation can be reported to the contract-violation handler, but if the handler returns normally, the program continues.

Both modes are useful for some builds. They exist intentionally, alongside the two terminating semantics. But for defensive programming, if your project needs a check to be present and terminating, you also need to prove that the relevant contracts are not evaluated with ignore or observe. This adds extra effort to a project.

For some projects, that cost may outweigh the benefits. So they might want to stay with the well-defined defensive-programming utilities they already have.

New implementation-defined behavior

A second point is implementation-defined behavior. Contracts add new instances of it. For example, the standard does not give you a portable way to select the evaluation semantic. In practice, you will probably tell the compiler or build system whether you want ignore, observe, enforce, or quick_enforce. But how you do that is implementation-defined. The result may even differ for different evaluations of the same contract assertion. If a terminating semantic is used, it is also implementation-defined whether contract termination calls std::terminate, calls std::abort, or terminates execution without further execution steps. If this sounds complicated, that is because it is. And, as discussed below, repeated evaluations are another implementation-defined part of the design.

So this is not just one switch to document. You need to know what the implementation does for your build, and which options were used to get that behavior.

In many projects, especially those with strict process requirements, implementation-defined behavior means documentation. You need to record what the compiler does, what options were used, and why that is acceptable.

This adds cost to adoption. For some projects, that cost may outweigh the benefits.

This also does not cover how compilers, linkers, and build systems expose and manage these choices. That infrastructure is not specified by the C++ standard, and it is not likely to become part of the C++ standard in the foreseeable future.

Uncertain evaluation-count behavior

The third point is evaluation count. The wording does not simply guarantee that a contract assertion is evaluated exactly once. Contract assertions may be repeated an implementation-defined number of times under the current wording. The recommended practice is that no repeated evaluations should be performed by default, but recommended practice is not a guarantee.

This is due to the flexibility contracts give you. You can, in principle, evaluate the same contract assertion with different semantics in different places. This flexibility has a price and brings its own complexity.

Some projects do need to care about this detail. If a predicate is expensive, or if it is used in a real-time context, "maybe more than once" is not just a theoretical concern.

It also makes reasoning about the cost of a contract assertion more difficult. You need to look at the code, and at what your build is doing, to see the final result.

But you can still use contracts

Contracts can put documentation into the interface. This can be useful for humans, tools, and AI. They can also be useful for debug builds. And for your safety requirements, you can keep using your existing facilities.

This combined approach is, of course, valid. But the question remains: if we have to keep our existing safety utilities for defensive programming, is the additional work worth the cost?

Summary

Of course, people can argue about all those details, and they did. They still do.

Some people try to explain away these concerns and blame the projects or people who decide against the use of contracts. I do not think that helps.

I do not want to go too much into detail here. I just want to mention a few points that I think are relatively simple to understand.

Will C++29 solve some of the problems? After all, Contracts in C++26 are often described as an MVP.

Probably partially, but not fully. There are interesting points of view about what an MVP is. The authors of P2900 explain what minimal means in this context. Minimal means that some things were intentionally left out while keeping the result viable. That explains the "minimal" in MVP.

I do not think the points mentioned above are easy to fix in the future. And whatever is added, I have a hard time seeing how it will make the topic simpler. And I have not even mentioned the infrastructure topic in detail, which I might do in a future post.

One observation is hard to ignore. The contract topic is complicated. And complicated things are often a problem. Is it worth the price?

Disclaimer

This post was written by a human. AI was used for language polishing.


If you find this page useful, donate a coffee to the C++ user group of Stockholm, SwedenCpp