Month: September 2026

Assert? Yes, but actually No

Everywhere I worked, assertion caused mixed feelings. On one side, they are a valuable tool to … well, assert that required conditions are respected; on the other side, they may crash your running program, spoiling the fun.

There are many ways to mitigate the problem and guidelines to make this tool more effective, but in the end, you have to decide whether you want assertions checked in your release build or ignored.

Why do assertions cause the execution to stop? The rationale is that when an assertion fails, a bug has manifested and the software’s behavior is wrong. Better stop than wrong.

The first rule to follow is to use assert for contract enforcement, not for error checking. If you are reading data from a serial line, you should expect spurious or unwanted characters to come down to your software. You do error checking, discarding or recovering data; you never assert there.

Also, the standard implementation may not be the best one for your needs. Usually, I define my own assert that, when running in the debugger, halts execution if it fails. Also, my custom assert for firmware and embedded software tries to save flash space by just reporting the address of the assertion (then the good programmer looks up in the listing file to see what went wrong).

I tend to agree that a failed assertion should stop (or restart) the application. Indeed, if a precondition or an invariant is not fulfilled, anything could have happened, and you may get crashes in unrelated operations, no crashes but wrong results, and security exploits. One possible outcome could even be the right behavior, but it is like sorting a sequence with bogosort.

If you prefer the application to be Terminator-like and keep going, you have a couple of options. You could log the fail, possibly with a full stack trace, so that the future you can be able to debug. Another option is to throw an exception and let the catcher decide what to do. This can be valuable in an application that performs several operations: an operation fails, but other operations may keep succeeding. Lastly, you can disable assertion checking, and this is possibly the worst thing you can do.

Pondering assertions lately brought me to the following conclusion – programming by contract is flawed. It is good to define a contract under which your code properly works, but it is bad that you can only detect that the contract has been breached after it has been breached. The contract is discovered to be void at the worst time possible – runtime. The whole point of having compiled code (syntax, type, and semantic checking done ahead of execution time) is thwarted by a failed assertion.

Wouldn’t it be good if we could prove the code won’t breach the contract at compile time?

Well, actually this is possible to an extent, and the language gives us a hint on how to do it. If a function accepts a std::string, you do not need to assert that you actually got a std::string. This is because no one could call that function with, say, an int and get the code to compile.

// contract: I want a std::string.
void doSomething( std::string const& s );
void somewhereElse()
{
  doSomething( 3 ); // compile time error, the contract is breached
}

So the key is in leveraging types to define contracts. Let’s consider a fictional code, where you want a function to accept an even number. The trick is to define a class that encapsulates an integer and can be constructed only from even numbers:

(Note, C++ got all the defaults wrong, requiring decorating classes with an unbelievably verbose boilerplate of constexpr, noexcept, [[nodiscard]] and the like. For the sake of clarity, I’ll leave out all those, but they are required in production code)

class EvenNumber {
  private:
    int mValue;
    explicit EvenNumber( int value ) : mValue{ value } {}

  public:
    int getValue() const { return mValue; }

    static std::optional<EvenNumber> makeEvenNumber( int n ) {
      return n % 2 == 0 ? EvenNumber{ n } : {};
    }
};

To build only an even number, I made the constructor private and provided a factory method that returns an optional of EvenNumber. If you try to build EvenNumber with an even number, you get an optional with a value; otherwise you’ll get an empty optional.

This is indeed no magic; instead of checking where the value is used, I need to check where the value is produced. But this is convenient from several points of view:

  • The value is produced once, but possibly used several times – the cost of checking is lower;
  • At the usage location, I likely don’t know how to handle an invalid value, resorting to assert or throwing an exception. Being aware of the contract breach where the value is produced is likely to give us more options on how to deal with it, like… better safe than sorry.
  • No invalid data exists in the program; if data exists, then it is valid by design.

Regretfully std::optional is a bit broken, so you may still get an UB if you try to dereference an empty optional. But this can be addressed either by using monadic optional (such as ChefFun::Option), or by adopting stricter guidelines on checking std::optional where they are produced and avoiding using them to pass values around.

Wrapping integers and strings into Smart types (or refined types) is not that hard; some template metaprogramming may even come to help to deal with common cases.

Also, enum classes may be used to prevent misuse. A common case in firmware is the handling of the flash memory, which can be organized into segments. So a memory location is identified by a segment and an offset within the segment. Let’s say that you provide a function to read a memory location:

std::byte readFlash( uint32_t segment, uint32_t offset );

This is error-prone because nothing prevents the caller from messing up the segment and offset. Using enum classes, you can write:

enum class Segment : uint32_t {};

enum class Offset: uint32_t {};

std::byte readFlash( Segment segment, Offset offset );

The compiler won’t let you pass any integer to the arguments; you have to cast them into their types, making it impossible to swap them.

This is a contract that cannot be enforced by assertion since there is no way to tell what an uint32_t is in the intention of the programmer, not at run time (nor at compile time).

Back to what can be enforced by assertion, what about null pointers?

Can we enforce that a pointer be non-null by type? What a huge advance – we would get rid of tons of defensive code checking for pointer validity and possibly prevent some crashes.

Before proceeding, there is a non-null pointer type concept in GSL. This type prevents you from constructing a pointer from a literal nullptr, but doesn’t prevent you from assigning a nullptr:

gsl::non_null_ptr<T> p0{0}; // compile time error

T* p1 = nullptr;

gsl::non_null_ptr<T> p2{ p1 }; // ok

And if you try to dereference a gsl::non_null_ptr<T> that is a nullptr, you’ll get the program terminated (exactly as if an assertion would have been violated).

This shows another shortcoming – how many times do you initialize a pointer with a literal (besides nullptr for marking empty values)?

But we can do better; we can write our template with a factory method that constructs only non-null pointers, avoiding random termination on pointer dereferencing.

template<typename T>
class Ptr<T>
{
  private:
    T* mPtr;
    Ptr( T* ptr ) : mPtr{ptr} {}

  public:
    static std::optional<Ptr<T>> makePtr( T* p ) {
      return p != nullptr ? Ptr(p) : {};
    }
};

This template core looks promising – everything can be computed at compile time, and there is no overhead in copying and assigning.

As often happens, the devil is in the details. We want to be able to use it as a language native pointer. This means:

  • dereference the pointer (operator*, operator->);
  • use Ptr<T> where T* is expected (implicit conversion);
  • support const-correctness;
  • perform pointer arithmetic;
  • compare with other Ptr<T> or T*;

That’s a whole lot of features. Some of them are trivially implemented. Some are less trivial. For example, adding or subtracting an integer to a pointer gives you a pointer. Since you need to be sure that the pointer obtained in this way is valid, you cannot return Ptr<T>, but you have to wrap it in a std::optional:

std::optional<Ptr<T>> operator+( intptr_t offset ) const;

+= and -= are better avoided since their semantics may not be clear for edge cases:

char c = ‘x’;
Ptr<char> pc = Ptr<char>::make( &c ).value(); // here is ok, since &c is not nullptr for sure
intptr_t w = reinterpret_cast<intptr_t>(pc.get()); // w has the same value of c;
pc -= w;

What is the value of pc? Surely it cannot be nullptr. The operation just failed… silently?

For a complete implementation, you may look into the ChefType repository.

Back to our intent – we can use type safety as a compile-time alternative to assertions. Is this always the case?

Consider a timer object, it has three states: created, armed, and expired. The first transition from created to armed is under code control, while the transition from armed to expired is triggered by something outside our code. Now consider the following assertion –

void cancel( Timer&amp; t )
{
  assert( t.isArmed() );
  // ...
}

Is there a way to encode this assertion using types? Actually, no, because there is no way to change the type of the object to reflect its state. You can do it for the first transition:

class CreatedTimer;
class ArmedTimer;

void f()
{
  CreatedTimer ct{};
  // ...
  auto at = armTimer( ct, TIMEOUT );
  // ...
}

This is fine, and it is also useful – for example, the ArmedTimer class may expose a wait() function that is not provided by the CreatedTimer, preventing you from waiting on an unarmed timer.

However, the change from Armed to Expired is not under compile-time control since it depends on an asynchronous event occurring at run-time.

On the other hand, maybe we can code the timer with some defensive coding that also behaves when the state is not the required one.

I mean, when we get characters from an input, we would like to have only a valid sequence. How good! We could avoid checking for wrong inputs and avoid coding error messages. But we know that inputs can be anything, and we need to validate, accept what is acceptable, ignore what is ignorable, and recover in the other cases.

The same approach could be pursued for the timer – after all, triggering is an external event that may happen at any time. Even if we require that the timer is not expired, it may expire a split second after the check.