Part of C++26 Series Explore the series

This post is a part of my C++26 exploration series where I take a new feature and try to understand and explain with a simple example in hand. Today’s topic is Contracts. First we will simply try to understand what is the problem it is solving then try doing some assessment on value addition.

The Current Issue without Contract

So let us take a quick look at the following example. It is a code snippet taken from a ring buffer implementation. The full source code for the ring buffer can be found here.

This function receives a payload, stores the size of the payload in a header of type std::uint32_t and of size k_header_max_size. k_header_max_size is define as the following:

The header length is written first and then the payload is written into the buffer. So each data item consists of the following:

data_size = header + payload

The write function is part of the class ring_buffer which keeps track of the head, tail and used space using the following data members:

std::size_t m_head{};
std::size_t m_tail{};
std::size_t m_used{};

And the buffer storage is represented by the following vector of type std::byte:

And the max size of the ring buffer is given by the following in the ring_buffer class:

Now from main() we can do the following:

So first we write a data item of size 40 bytes and another of size 100 bytes. The total capacity of the buffer is 128 bytes whereas we have written total of 40 + 100 bytes of data plus their header size which is total of 8 more bytes. That makes it total of 128 bytes been written into the buffer. There is a function in the ring buffer class which can read the data written into the ring buffer as the following:

and another function which prints the ring buffer content:

The full source code is available here. Compile and run the code as follows:

So after the second write when we print the content of the ring buffer we can see the content has been overwritten. The reason being there is no check at the moment to verify of there is enough space in the ring buffer to accommodate new writes.

Current Solution

So we can do to prevent this? We can introduce a new function in the class which can give us the available space. The following is an implementation of available space:

Now we can call this available() function and check if there is space available for the write operation with the request size, and if not we simply do not allow the write operation. So a modified version of the write() function could be as follows:

Now with this modification in place if we compile and run it again we will see the following:

So the earlier issue of overwriting is gone. The additional check that we introduced in the write function is kind of voluntary and there is no possible way to enforce it.

The contracts way

Contracts in C++26 is giving us an option to make it a binding requirement at the function declaration. We can rewrite the write() function in the class in the following way using contracts in C++26:

Note the introduction of two precondition in the function declaration:

The first condition makes sure the record that being written is not empty and the second one makes sure there is enough space in the ring buffer to accommodate the new write.

Similarly, we can introduce some post condition in the read() function as the following:

The full source code is available here. We can compile and run this code like the following:

As you can see the restriction have been enforced and as the contract broken by the second attempt to write, the program has been terminated. Note while compiling the code we have used the following flag:

It enforces the pre and the post conditions. But there can be other modes of contracts as the following:

  • ignored: Contract checks are ignored.
  • observed: The condition is checked and the violation handler is called. Execution continues if the handler returns.
  • enforce: The condition is checked and the violation handler is called. The program then terminates.
  • quick_enforce: The condition is checked and the program terminates immediately without calling the violation handler.

Custom violation handler

We have not defined any violation handler so far. Let’s define the violation handler function called handle_contract_violation as follows:

Whenever a contract violation happens, the C++26 runtime will call the handle_contract_violation() with the reference to std::contracts::contract_violation. The std::contracts::contract_violation has the information like comment, location which contains the file_name() and line number etc. This handler is throwing a custom exception which is called contract_violation_error and defined as below:

The full source code can be found here. Now, if there is a contract violation your program need not necessarily need to be terminated, it can be handled as well.

What is the value add?

I have been trying to explore more on this but as far as I can see there are particularly two value addition by introduction of contracts:

One, the contracts, the pre and the post conditions are explicit, can be enforced with manual checks and assertion. This is great for writing clean code. Rather than having manual checks everywhere throughout the function, we can think it through, decide the binding requirements and then everything else will be taken care automatically.

Second, the API becomes lot more clearer. The function now explicitly tells the user what the pre and the post conditions are. Earlier the only way to communicate to the user was inline commenting and API documentation. Now with contract that would become lot more clearer to the user of the API and easier for the developer to communicate via the code itself.

Third, static analysis would be better with added explicit pre and post conditions in the declaration itself.

Anyone knows about any other benefits of contract please feel free to let me know.

The source code is available from this git repo.

C++26 Series

Follow my C++26 series as I explore the language’s new features through practical examples.
  1. 01 Compile Your First C++26 Program with GCC 16.1
  2. 02 C++26: What Is template for?
  3. 03 C++26: What Is Reflection and How Do You Use It?
  4. 04 C++26 Reflection: Simplifying JSON Serialization
  5. 05 C++26 Reflection Annotations: Automated Member Validation
  6. 06 C++26 Contracts: What Do They Add Beyond Manual Checks and Assertions?
  7. 07 C++26 in GCC 16 Zero-Fills Your Local Variables: Time to Recompile Your C++ Programs

Discover more from Tech For Talk

Subscribe to get the latest posts sent to your email.

Leave a Reply