Coder Social home page Coder Social logo

Comments (1)

mbeutel avatar mbeutel commented on September 22, 2024

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 from U, not_null<T> can be explicitly constructed from U. In particular, not_null<T> can be explicitly constructed from T.
  • The * and -> operators of not_null<T> forward to the respective operators of T.
  • The member function not_null<T>::get() forwards to T::get().
  • If T can be copied, not_null<T> can be copied. If T can be moved, not_null<T> can be moved.
  • If T (implicitly or explicitly) converts to a pointer-like type U, not_null<T> will also (implicitly or explicitly) convert to not_null<U>.
  • If T (implicitly or explicitly) converts to a type U, not_null<T> will also (implicitly or explicitly) convert to U. In particular, not_null<T> will (implicitly or explicitly) convert to T.

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 from T
  • not_null<T> does not implicitly or explicitly convert to bool.
  • Every constructor of not_null<T> (conversion, copy, move) uses gsl_Expects() to check that the argument is not nullptr.
  • 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> uses gsl_Assert() to check that the enclosed object is not nullptr.

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 a nullptr (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 nullptrs 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 a T 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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.