Comments (1)
Thank you for the report.
not_null<shared_ptr<T>>
should implicitly convert to weak_ptr<T>
. As you point out, the conversion operator currently fails to compile, which happens because it makes the unfounded and unnecessary assumption that the type being converted to is also pointer-like, which is easily corrected (as done by #311).
In retrospect, my use of gsl_Ensures()
in the member functions of not_null<>
was misguided; these checks express class invariants, not postconditions, and should therefore be written using gsl_Assert()
(though that didn't exist yet when they were introduced).
The confusion with regard to not_null<>::get()
, and with not_null<>
generally, arises because gsl-lite lacks documentation (cf. #198). Instead of addressing your confusion directly, I'll try to knock up a draft for a chapter on not_null<>
. Any comments are appreciated.
gsl_lite::not_null<>
gsl_lite::not_null<>
is a wrapper class template for pointer-like types such as raw pointers and smart pointers which establishes the additional quasi-invariant that the enclosed pointer-like object references a valid object.
Motivation
C++ knows two types of indirections: pointers and references. A pointer can refer to a special value known as nullptr
, which indicates it does not point to any object, and can be subsequently reassigned. In contrast, references must always refer to a valid object, and they can be assigned only once, as part of their initialization.
When defining function signatures, it is therefore customary to use pointers and references to indicate whether an object reference is required or optional:
void lock( Mutex & m ); // requires a valid object reference
struct ListNode
{
ListNode* prev;
ListNode* next;
int payload;
};
void remove( ListNode * x ); // also accepts a `nullptr`
But this convention does not apply to every situation. For example, accepting a pointer argument can also emphasize the fact that the object's memory address may be taken and stored, as in the case of a list or tree node. Storing the memory address of a by-reference argument "feels" wrong, and may be flagged by static analyzers:
void insertAfter( ListNode & x, ListNode & newNode )
{
newNode.prev = &x; // <-- Eww.
newNode.next = x.next;
x.next = &newNode; // <-- Eww.
}
The function would be less awkward if it accepted pointers.
To ensure a pointer argument is not nullptr
, we'll add a precondition check to the function:
void remove( ListNode * x )
{
gsl_Expects( x != nullptr );
if ( x->prev != nullptr ) x->prev->next = x->next;
if ( x->next != nullptr ) x->next->prev = x->prev;
delete x;
}
There are other situations when an object cannot be passed by reference, but a nullptr
may still be unwanted. The most familiar case is passing ownership, best expressed with a smart pointer:
void insertAfter( ListNode * x, std::unique_ptr<ListNode> newNode )
{
gsl_Expects( x != nullptr );
gsl_Expects( newNode != nullptr );
newNode->prev = x;
newNode->next = x->next;
x->next = newNode.release();
}
Writing all the precondition checks against nullptr
quickly becomes tedious.¹ And unlike the contract checks once envisioned for C++20, the gsl-lite precondition checks are not part of the function signature, which therefore does not convey that it cannot handle nullptr
input.
This is where gsl_lite::not_null<>
comes in. With not_null<>
, the precondition can be "lifted" into the type system, and thus into the function signature:
void remove( gsl_lite::not_null<ListNode*> x )
{
if ( x->prev != nullptr ) x->prev->next = x->next;
delete x;
}
All not_null<>
constructors check their arguments for nullptr
with gsl_Expects()
, so the functions above can already assume that their arguments will never be nullptr
, and the explicit precondition checks can be omitted.
not_null<>
can also be used with smart pointers, so the function signature of insertAfter()
could be changed to
void insertAfter( gsl_lite::not_null<ListNode*> x, gsl_lite::not_null<std::unique_ptr<ListNode>> newNode );
Definition
not_null<T>
attempts to behave like the underlying type T
as much as reasonably possible:
- If
T
can be (implicitly or explicitly) constructed fromU
,not_null<T>
can be explicitly constructed fromU
. In particular,not_null<T>
can be explicitly constructed fromT
. - The
*
and->
operators ofnot_null<T>
forward to the respective operators ofT
. - The member function
not_null<T>::get()
forwards toT::get()
. - If
T
can be copied,not_null<T>
can be copied. IfT
can be moved,not_null<T>
can be moved. - If
T
(implicitly or explicitly) converts to a pointer-like typeU
,not_null<T>
will also (implicitly or explicitly) convert tonot_null<U>
. - If
T
(implicitly or explicitly) converts to a typeU
,not_null<T>
will also (implicitly or explicitly) convert toU
. In particular,not_null<T>
will (implicitly or explicitly) convert toT
.
In fact, the easiest way to reason about not_null<>
is to pretend it was defined as
template< typename T > using not_null = T;
gsl_lite::not_null<>
differs from this trivial definition in the following ways:
not_null<T>
has no default constructor.not_null<T>
cannot be implicitly constructed fromT
.²not_null<T>
does not implicitly or explicitly convert tobool
.- Every constructor of
not_null<T>
(conversion, copy, move) usesgsl_Expects()
to check that the argument is notnullptr
. - If
T
is move-constructible, then on every attempt to access the enclosed object (be it through*
,->
,get()
, or with a conversion),not_null<T>
usesgsl_Assert()
to check that the enclosed object is notnullptr
.
Under normal conditions (that is, if precondition checks are enabled and configured to either terminate the program or raise an exception), these properties condense to a single guarantee:
- A
not_null<T>
object will never hand you anullptr
(be it through*
,->
,get()
, or with a conversion).
TODO: comment on the rationale behind this not_null<>
definition
Corollaries
An oft-overlooked corollary of these properties is that not_null<>
cannot be probed for nullptr
. Checking whether a not_null<>
holds a nullptr
will either fail to compile or trigger a runtime precondition violation:
auto p = std::make_unique<int>();
auto nnp = not_null( std::move( p ) );
if ( nnp ) { } // compile error: no explicit conversion to `bool`
auto nnq = std::move( nnp );
bool isNull = nnp.get() == nullptr; // always raises precondition violation
This is by design; if checking for nullptr
is something you need to do, don't use not_null<>
.
Although not_null<T>
approximates the interface of T
, it does not reproduce it exactly. In particular, additional member functions of T
are not available on not_null<T>
:
gsl_lite::not_null<std::unique_ptr<ListNode>> node = gsl_lite::make_unique<ListNode>(); // (TODO: current version still returns `std::unique_ptr<ListNode>`!)
ListNode* rawNodePtr = node.release(); // <-- error: no member function `not_null<>release()`!
To access these member functions, first use gsl_lite::as_nullable()
to obtain the underlying pointer-like object:
std::unique_ptr<ListNode> nodePtr = gsl_lite::as_nullable( std::move( node ) );
ListNode* rawNodePtr = nodePtr.release();
The function body of insertAfter()
would thus read:
void insertAfter( gsl_lite::not_null<ListNode*> x, gsl_lite::not_null<std::unique_ptr<ListNode>> newNode )
{
newNode->prev = x;
newNode->next = x->next;
x->next = gsl_lite::as_nullable( std::move( newNode ) ).release();
}
Also, note that not_null<T>::get()
is defined to forward to T::get()
. It does not always return a raw pointer; in fact, it is not defined at all for T = X*
(because pointers have no member functions), and it returns whatever T::get()
returns, which may or may not be a raw pointer. To access the underlying pointer of a not_null<T*>
object, call gsl_lite::as_nullable()
.
Quasi-invariants
If the constructor already keeps nullptr
s away, why does not_null<T>
have to check the enclosed object for nullptr
on every access? – The answer is that C++'s move semantics are non-destructive, and hence it is possible to end up with a not_null<T>
that holds a nullptr
quite easily:
template< typename T >
void drop( T & x ) // borrowed from Rust
{
T y = std::move( x );
(void) y; // suppress warning about unused value
}
void nullInNotNull( gsl_lite::not_null<std::unique_ptr<ListNode>> x )
{
drop( x );
// `x` is now `nullptr`
}
not_null<T>
therefore cannot guarantee that it will never hold a nullptr
. However, it can make sure to never hand you a nullptr
; for movable types, that requires a nullptr
check in every accessor function.
The function drop()
leaves its argument in the moved-from state, sometimes affectionately referred to as the zombie state. The language requires that the moved-from state of any object be valid, which only means that the object destructor must be able to run normally. There are two schools of thought with regard to the meaning of the moved-from state:
- Traditionalists say that the moved-from state should be a state like any other. Any precondition-free operation that is valid for a regularly constructed object of type
T
should also be valid for aT
in moved-from state. In fact, the moved-from state should be equivalent to the default-constructed state.
This view can be naturally satisfied by a container class, which default-constructs to an empty container. Most classes in the C++ standard library follow this approach (e.g. the I/O stream classes, threads, containers in most implementations).
TODO: describe the opposing viewpoint, refer to https://www.justsoftwaresolutions.co.uk/cplusplus/invariants.html
Composition
TODO: this should be a chapter on how not_null<>
makes nullability composable when defining resource handles
¹ A familiar sorrow for .NET programmers.
² If this is inconvenient for you, you can use gsl_lite::not_null_ic<T>
, a subclass of not_null<T>
that supports implicit construction from T
.
from gsl-lite.
Related Issues (20)
- `not_null<>` should be hashable HOT 2
- Should `not_null<>` be two different classes? HOT 3
- "illegal instruction" Error when using gsl::span as Kernel Parameter HOT 3
- Test compilation failures on GCC 11: error: array subscript -1 is outside array bounds of 'char[6]' [-Werror=array-bounds] HOT 7
- Test failures on s390x (GCC 10.2.1 and 11.0.1) HOT 7
- Support `narrow<>()` for types without total order
- gsl_FEATURE_GSL_LITE_NAMESPACE not working HOT 3
- Definition of `fail_fast` and of `narrowing_error` triggers `clang`'s `-Wweak-vtables` HOT 2
- gsl_CONFIG_UNENFORCED_CONTRACTS_ASSUME in kernel code HOT 1
- including gsl-lite triggers `-Wuseless-cast` warnings HOT 2
- Add tests for `not_null_ic<>`
- Tests fail to compile due to -Werror=type-limits when char is unsigned HOT 6
- Add CI for non-x86 architectures
- Allow `not_null<void*>` and related. HOT 9
- `not_null`'s evaluation of nullness can lead to *very* cryptic errors. HOT 10
- Suppress warnings in lest via system include
- Comparison of `std::optional<not_null<T>>` and `not_null<T>` is ambiguous HOT 2
- gsl::span works differently than std::span in terms of type matching? HOT 2
- 0.41.0: not ready to be build with `-D_FORTIFY_SOURCE=3` HOT 17
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from gsl-lite.