![]() |
Arene Base
Fundamental Utilities For Safety Critical C++
|
A common source of defects in code that deals with pointers, both raw and fancy, is the accidental violation of assumptions made in a system that a pointer is not null. At best, the requirement that a pointer not be null is documented in its API documentation. However this requirement is not validated by any static analysis. It may be validated by runtime checks, however this is not robust: developers often forget them, or may assume that other logic has already checked the invariant when this is not the case.
A more robust solution to this problem is to encode the requirement that a pointer not be null into the type system. With such a type:
nullptr can be hoisted to a compile-time error.The name of this type is non_null<P>, where P is any "pointer-like" type for which a comparison to nullptr would be meaningful.
The type consumes a single template parameter, P. This represents the pointer-like type to which the non_null invariant is to be enforced. To qualify as pointer-like, the type must at minimum:
operator->() and operator*().operator!=(const P&, std::nullptr_t) or have that operation be valid through implicit conversion.non_null<P> models P, and thus owns the pointer it holds.
As there is no reasonable default for a pointer which cannot be null, non_null<P> is not default-constructible and is thus a non-regular-type.
non_null<P> can be constructed/assigned from any type U which is convertible to P, as well as any non_null<U> where U is convertible to P. If P is copyable, then non_null<P> is copyable. Construction must be explicit as P may be nullptr but non_null<P> may not and thus the conversion is not inherently valid.
Construction from the literal nullptr is delete'd. In addition, attempting to construct a non_null<P> from any value which compares equal to nullptr is an ARENE_PRECONDITION violation.
Examples of constructing non_null:
Note that in practice, you rarely need to construct non_null directly. Instead, there are various factory helpers, discussed in detail below. In addition, there are convenience aliases for specifying the type in a less verbose manner when the type must be explicitly specified.
non_null<P> provides the following member types, for compatibility with various standard pointer-trait facilities:
non_null<P>::element_type, which is std::pointer_traits<P>::element_type.In addition, it defines the following:
non_null<P>::held_pointer, which is P.non_null<P>::pointer, which is the type returned by non_null<P>::get().non_owning_ptr provides general API uniformity with existing stdlib smart pointer types:
auto non_null<P>::get()->pointer: returns a raw pointer to the pointed-to element.void non_null<P>::reset(U ptr): replaces the held pointer with a new pointer of any type U that is convertible to held_pointer and that is not nullptr.As non_null<P>::get() models P::get(), a separate interface is needed to obtain a reference to the underlying pointer. This is provided by the non_null<P>::unwrap() method. For fancy pointers, unwrap() provides const and rvalue overloads that return const P& and P&& respectively. For raw pointers, unwrap() returns P.
Examples:
Note that it is not possible to obtain P&; doing so would allow users to accidentally violate the invariant of non_null through direct assignment to the underlying pointer.
As non_null<P> models P, explicit conversion to bool is supported, as well as the ! operator, and has meaning equivalent to P.
In addition, for ergonomic convenience non_null<T> supports:
const P& and P&& for fancy pointers, and P for raw pointers. This allows it to be "dropped in" to existing codebases without creating high water marks for converting all downstream users to non_null<P>.const non_null<P>& to U if std::is_constructible<U, const P&>.non_null<P>&& to U if std::is_constructible<U, P&&> and P is a fancy pointer.Here's a simple example showing how this functionality allows incremental changes:
As non_null<P> models P, it supports 6-way comparison with semantics equivalent to comparing P.
For the comparisons to other types, the non_null type is accepted as a template parameter NN, which is constrained to be exactly the current instantiation of non_null. This prevents implicit conversions, and thus requires that one of the operands is a non_null type, avoiding ambiguous overload resolution and potential infinite recursion evaluating constraints like is_equality_comparable.
non_null<P>::operator->() and non_null<P>::operator*() are provided and simply pass-through to the held P.
In order to provide improved safety, operator-> and operator* are "checked" operations when P is not a raw pointer, to ensure the invariant of non-null has been maintained. See below for additional details on why this is needed.
Ordinarily, non_null<P> cannot become nullptr; it cannot be constructed from nullptr, reset() to nullptr, and there is no way to get a non-const lvalue reference to the held pointer.
However, if P is a fancy pointer, in order to be movable, it is not practical to prevent the held pointer from becoming nullptr. Otherwise, moving a non_null<shared_ptr<T>> would actually result in a copy, and non_null<unique_ptr<T>> would be neither copyable nor movable, making it non-functional in real programs. The relaxing of this constraint for fancy pointers means that if a non_null of such a type is dereferenced or similar after being moved from, the pointer may be nullptr.
However, most safety/coding standards ban use-after-move. clang-tidy also has a check to detect use-after-move statically, bugprone-use-after-move which is very reliable. Given this, allowing the invariant to be violated for fancy pointers in a moved-from non_null<P> is considered an acceptable tradeoff. For additional safety, for any API which returns the held pointer or dereferences it, an ARENE_INVARIANT validates the held pointer is not nullptr. Violation of the invariant results in process abandonment.
This check does incur a small performance penalty; the impact of this check is mitigated in many cases with optimizations enabled as the compiler will remove redundant nullptr checks.
In order to provide consistent ergonomics with existing smart pointers, the following convenience alias templates are provided:
In order to facilitate easier adoption of non_null_shared_ptr and non_null_unique_ptr, equivalents of std::make_shared<T> and std::make_unique<T> are provided which return non_null_shared_ptr<T> and non_null_unique_ptr<T> respectively. They have all the same advantages as their stdlib counterparts, and are intended to be "drop in replacements." As there are no situations where std::make_[shared|unique] can ever return nullptr, all usage in a codebase can and should be replaced with these helpers.
Examples:
There is also a factory to create non_null_ptr<T> with argument deduction from the input pointer for C++14 contexts:
For Jama requirements, please see Arene_Eco_System-L5SW-1227