share article

The challenges posed by C++ standard library headers and in safety-critical systems

Feature

C++ is a popular programming language due to its performance, flexibility and low-level control. However, when it comes to handling complex data structures, developers often face a series of challenges that can complicate safety-critical applications, such as in automotive.

Take <tuple>, for instance. This C++ header provides a powerful and versatile data structure that allows developers group different types of data into a single entity without the overhead of defining a custom class. In automotive systems, for example, this can be invaluable – whether representing sensor readings, vehicle states, or configuration parameters for control systems.

Likewise, the <chrono> header from the C++ standard library provides applications with various types to abstract clocks, points in time and operations thereon. It helps developers reason about time in a clean, type-safe manner, essential for systems with tight timing constraints.

However, there are dangers lurking beneath some of its convenient features. The C++ Standard has some simple-yet-strong mathematical statements that do not always hold in practice with the limited and variable precision of floating-point numbers.

Both headers offer powerful abstractions, namely time manipulation via <chrono> and flexible, type-safe data structures through <tuple>. Such abstractions aim to hide away conversions prone to complex precision errors and wrestle the convoluted overload resolution rules. In this article, we use examples uncovered during the development of SuperGuard, functional safety test suite for C++ by Solid Sands, to demystify the potential pitfalls of both headers. We take a closer look at the nuances of constructor resolution and time representation, explain why they matter, and show how automated testing plays a vital role in making C++ safer for critical systems.

Functional safety and the role of testing

In safety-critical domains such as automotive, aerospace or industrial control, software must comply with stringent requirements. Standards like ISO 26262 specify how software should be designed, tested and verified. These standards demand not only functional correctness, but also predictability, traceability and comprehensive testing across all software layers, including the language and library level.

The standard library of C++ is often assumed to be safe and reliable, but, in practice, it is just as susceptible to corner cases and implementation differences as application-level code. Particularly in the areas of generic programming and numerical computation, small deviations from expected behaviour can have large consequences.

Solid Sands created SuperGuard to subject the standard C++ library to the same level of scrutiny typically reserved for application logic. By identifying ambiguities, verifying conformance and exposing hidden behaviours, SuperGuard provides the depth of analysis required for use in certified systems.

The versatility and power of <tuple>

The tuple is one of the most powerful data structures in C++, allowing developers to combine related data without declaring a full-blown class. A good example is a tuple that holds the position of a vehicle in a 3D space (x, y, z coordinates) or a tuple that represents the state of various sensors in an autonomous driving system. Tuples avoid unnecessary performance overhead, eliminate boilerplate code for conversion, and retain type safety – all critical factors for automotive applications where both efficiency and reliability are non-negotiable.

Although tuples offer significant advantages, they are not natively part of the C++ language itself. Instead, they reside in the C++ Standard Library under the <tuple> header. This header relies heavily on template programming and introduces a level of complexity that, while powerful, can create challenges for developers, especially when it comes to testing for functional safety.

In automotive software development, tuples are used in various systems, from sensor fusion algorithms in autonomous vehicles to control software for engine management. Ensuring that tuple construction and conversions are handled correctly is therefore crucial for code ergonomics, efficiency and preventing subtle compile-time issues that could indirectly impact system reliability.

C++ allows tuples to be constructed in a variety of ways. These include:

  • From the tuple’s element types directly;
  • By copying or moving from a similar tuple;
  • From a pair or other combinations of types convertible to the tuple’s elements.

This flexibility mainly introduces complexity in the implementation and testing of <tuple>. The issue lies in the way these constructor calls are resolved, such as in a situation where a user-defined type A can be implicitly converted from a tuple of A. Constructing a tuple from another tuple or type could be interpreted as copying or moving, or it could trigger a conversion from a compatible type.

Such scenarios create potential ambiguities in constructor resolution. In cases where multiple constructor options exist, the compiler must decide which constructor to invoke. This process, called overload resolution, is part of C++’s type system but can quickly become complex, especially when implicit conversions are involved. If the programme can’t unambiguously resolve the constructor, the result is a compilation error.

SuperGuard addresses these complexities through meticulously crafted tests that ensure overload resolution selects the intended constructor, providing confidence that tuple construction behaves correctly under all scenarios. In automotive systems, where functionality must be thoroughly validated before deployment, relying on such tools is essential. SuperGuard ensures that tests for C++ tuple constructors are not only thorough but also unambiguous. It handles all the details of constructor resolution, so developers don’t need to worry about unexpected behaviours or errors when dealing with complex tuple types.

What makes SuperGuard particularly useful in automotive testing is its ability to simulate various constructor scenarios, ensuring that all edge cases are covered. For instance, in the example where a tuple is constructed from a convertible type A, SuperGuard makes sure that the correct constructor is selected, taking into account both explicit and implicit conversions. Without this careful attention to detail, testing might miss critical issues or lead to inconclusive results.

Time conversions and floating-point pitfalls in <chrono>

The <chrono> header provides users with ways to implicitly convert between their own time representation and the implementation-defined representation used by <chrono> clocks. For instance, a query to “wait for 0.0025s” is converted with little-to-no effort to “2,500,000ns” to interface with a clock.

The straightforward way to perform these conversions is to compute them directly. This is inherently a lossy operation, a limitation that is well documented by the C++ Standard. During comparisons between differently represented points in time, <chrono> attempts to ensure greater accuracy with an additional step. Instead of converting one side to the other, both are converted to a time unit that is a common fraction between the two.

This means that their values can only be multiplied. Yet the multiplication required to establish this common representation can result in large numbers where floating-point representations may begin to lose precision. This is a common issue, particularly when most implementations count time in nanoseconds.

Solid Sands has found that the Standard and most implementations overlook this quirk in the floor() function when using floats. The floor() function converts a time point from one representation to another and rounds it down. The result must be the latest representable time point that is not later than the original. This is asserted using an operator, which converts operands to a common representation. If the implementation does not account for the potential loss of precision, the next representable time after rounding down may become equal to the original – even if it is larger in theory. The post-condition of floor() would then be violated.

This issue, and more besides, is revealed through the use of a tool like SuperGuard. It causes subtle differences in results and appears seemingly arbitrarily in specific use cases and configurations. Because such discrepancies are not explicitly documented in the function specifications, they can easily go unnoticed – a serious concern in safety-critical systems.

Correct implementation

The complexities of constructor resolution and type conversions pose challenges, especially in safety-critical systems. Indeed, <tuple> primarily provides performance and ergonomic benefits; nevertheless, ensuring its correct implementation remains crucial. With SuperGuard, every case is thoroughly considered, documented and tested. SuperGuard handles the fine-grained details, giving engineers peace of mind when using the <tuple> and <chrono> headers in a safety-critical project and making a case for robust verification in critical software systems.

By Max Blankestijn, Software Engineer, Solid Sands 

Share this article

Related Posts

View Latest Magazine

Subscribe today

Member Login