EmbeddedRelated.com
Blogs

C++ Assertion? Well Yes, But Actually No.

Massimiliano PaganiApril 8, 2024

Assertions are a simple tool you can use to enforce required conditions. Let's say you have a function that computes the square root of a number. Possibly nothing good would happen should this function be called with a negative argument. Maybe you even stated in the documentation for this function that it has to be called with a non-negative argument, but the program is running and ops, the function is presented with -1. By inserting an assertion on the function argument you make sure that your requirement on the function argument is respected:

#include <cassert>
float squareRoot( float n ) {
  assert( n >= 0.0f );
  // implementation
}

Now if the function is called with -1, the assertion intercepts the problem and causes the program termination.

Everywhere I worked, assertions caused mixed feelings. On one side they are a valuable tool to make sure that required conditions are respected during code execution, on the other side, they may crash your running program spoiling the fun.

There are many ways to mitigate the problem and some guidelines to follow to make this tool more effective, but in the end, you have to decide whether you want assertions to potentially stop one or more of the functionalities of your release build, or be ignored.

Why do assertions cause the execution to stop? The reason is that it is better to have no result at all than a wrong one. When an assertion fails, a precondition no longer holds. Software can't do what was intended. In other words, a bug has been caught, and there is no reasonable course of action for progressing or recovering. Moreover, you don't know exactly where the bug originated, I like the mental picture of your function being the only structure alive in a bug-nuked land.

I tend to agree that a failed assertion should stop (or restart) the application. Even if the precondition-protected code doesn't crash, you may get crashes in unrelated operations, no crashes, but wrong results, and security exploits. One possible outcome could even be the right or acceptable behavior, but it is just luck.

Also, if used pervasively, assertions can catch bugs very close to where they are, saving quite a lot of investigation time needed to navigate back from the effect to the cause of the wrong behavior.

Still, I empathize with those who don't want occasional run-time crashes of their application, and surely assertions must be used with a grain of salt to avoid unnecessary unstable software.

Assert is for contract enforcement only, and 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, and you never assert there.

Also, the standard implementation, may not be the best for your needs. In the past, I usually defined my custom assert with the following properties -

  • if run in the debugger the failed assertion stops the debugger, making it easy to spot problems during the development;
  • when compiled for embedded software, only the address of the failed assertion was printed; saving flash memory from being filled with assertion strings (you could always look up the offending line in the listing file);

If you prefer the application to be a never-stopping Terminator-like, you have a couple of options. You could log the fail, possibly with a full stack trace so that in the future you can be able to investigate. Another option is to throw an exception and let the catcher decide what to do. This can be valuable if the application performs several independent operations - the failure of an operation due to an assertion, would not prevent other operations from succeeding. Last you can disable assertion checking entirely,  but this is possibly the worst thing you can do.

Pondering on assertions recently brought me to the following conclusion – this implementation of 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 – the run-time. 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, 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 did get a std::string. Indeed, thanks to the type system, 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. There is no built-in type that defines an even number and you may be tempted to use a language integer to encode such a type. If you resist the temptation, you can define a class that encapsulates an integer and can be constructed only from even numbers:

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 } : {};
    }
};

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

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 building an EvenNumber with an odd number you get an empty optional.

This is indeed no magic, instead of checking the contract 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 usage location, likely, I 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 a data exists, then it is valid by design.

Regretfully (C++17/20) std::optional lacks of critical functions so you may still get an Undefined Behavior if you try to dereference an empty optional. If you can't use C++23, this can be addressed either by using monadic optional (such as ChefFun::Option or Sy Brand's optional), or by adopting stricter guidelines on checking std::optional where they are produced and avoid to used them to pass values around.

Wrapping integers and strings into Smart Types (also called Refined Types) is not that hard, some template metaprogramming may even come to help in dealing with common cases.

The trick is to use the class as a transport from the producer to the consumer and map only a minimum set of operations from the underlying type.

Let's consider the C and C++ programmer arch-enemy - the null pointer.

Can we enforce that a pointer be non-null by type? That would be great – we could address the one-billion-dollar mistake, eliminate tons of defensive code checking for pointer validity, and possibly prevent some crashes.

Before proceeding, it is worth mentioning that 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::not_null<T*> p0{0}; // compile time error
T* p1 = nullptr;
gsl::not_null<T*> p2{ p1 }; // ok

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

We can do better than that, 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>> make( T* p ) {
      return p != nullptr ? Ptr(p) : {};
    }
};

This core looks promising – everything can be computed at compile time and there is no overhead in space or time, but for the std::optional.

Now we need to add some methods to mimic the pointer behavior so we can use Ptr like we would use T*.

More precisely we want -
  • 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;

Here we have a sort of conundrum - pc is a valid object whose value is nullptr, but since we cannot have this value for pc type, then it is not a valid object. The proper action for pc would be to turn it into an empty std::optional, but the -= syntax doesn't allow for that.

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

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

Well, regretfully, no.

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& t )
{
  assert( t.isArmed() );
  // ...
}

Is there a way to encode this assertion using types? Actually no, because types are fixed at compile time and here the property of the object we want to check has changed at run-time. 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 programming that behaves also when the state is not the required one.

Time for a wrap-up. In this post, we have seen why it is good to have contracts and enforce them. The traditional C++ approach has been using either the standard library assert or a custom assert-like function/macro to provide more functionality and flexibility. Aborting the application on a failed precondition is a correct, but undesirable solution.

By leveraging the type system of the language, it is possible to express contracts using types. This approach allows the compiler to prove that no contract will be breached during the execution of the program. Contracts get enforced by the language, making it impossible to get code compiled into something that may fail an assertion. An example of a non-null pointer type has been presented, showing that the approach is practical.

Unfortunately, the approach can't always be applied. If contracts are about states or conditions changing during the program execution, there is no way to encode this information into types.

Anyway, these cases can be worked around by having more resilient code capable of properly handling objects not in the required state.



To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: