Gsl::Not_Null<T*> VS. Std::Reference_Wrapper<T> VS. T&

gsl::not_nullT* vs. std::reference_wrapperT vs. T&

References are not pointers that cannot be null. References are semantically very different to pointers.

References have value assignment and comparison semantics; that is, assignment or comparison operations involving references read and write the referenced value. Pointers have (counterintuitively) reference assignment and comparison semantics; that is, assignment or comparison operations involving pointers read and write the reference itself (i.e. the address of the referenced object).

As you noted, references cannot be rebound (due to their value assignment semantics), but the reference_wrapper<T> class template can be rebound, because it has reference assignment semantics. This is because reference_wrapper<T> is designed to be used with STL containers and algorithms, and would not behave correctly if its copy assignment operator didn't do the same thing as its copy constructor. However, reference_wrapper<T> still has value comparison semantics, like a reference, so it behaves very differently to pointers when used with STL containers and algorithms. For example, set<T*> can contain pointers to different objects with the same value, while set<reference_wrapper<T>> can contain a reference to only one object with a given value.

The not_null<T*> class template has reference assignment and comparison semantics, like a pointer; it is a pointer-like type. This means that it behaves like a pointer when used with STL containers and algorithms. It just can't be null.

So, you are right in your assessment, except you forgot about comparison semantics. And no, reference_wrapper<T> will not be made obsolete by any kind of pointer-like type, because it has reference-like value comparison semantics.

Does gsl::not_null hurt performance?

But, it also adds an overhead because methods operator->() and operator*()

Except, those functions are defined inline and are extremely small, thereby optimiser will (very likely) expand them inline, which would remove that potential overhead entirely.

how did the doc conclude the net performance gain is positive?

As you quoted, the document doesn't even acknowledge associated overhead, so such conclusion is trivial.

If you mean how did the authors of the document come to such conclusion, only those authors know. It may range from "they measured its effects" to "they made an assumption".

Why hasn't not_null made it into the C++ standard yet?

There is one big technical issue that is likely unsolvable which makes standardizing not_null a problem: it cannot work with move-only smart pointers.

The most important use case for not_null is with smart pointers (for raw pointers a reference usually is adequate, but even then, there are times when a reference won't work). not_null<shared_ptr<T>> is a useful thing that says something important about the API that consumes such an object.

But not_null<unique_ptr<T>> doesn't work. It cannot work. The reason being that moving from a unique pointer leaves the old object null. Which is exactly what not_null is expected to prevent. Therefore, not_null<T> always forces a copy on its contained T. Which... you can't do with a unique_ptr, because that's the whole point of the type.

Being able to say that the unqiue_ptr consumed by an API is not null is good and useful. But you can't actually do that with not_null, which puts a hole in its utility.

So long as move-only smart pointers can't work with not_null, standardizing the class becomes problematic.

What is the difference between std::reference_wrapper and a simple pointer?

std::reference_wrapper is useful in combination with templates. It wraps an object by storing a pointer to it, allowing for reassignment and copy while mimicking its usual semantics. It also instructs certain library templates to store references instead of objects.

Consider the algorithms in the STL which copy functors: You can avoid that copy by simply passing a reference wrapper referring to the functor instead of the functor itself:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

This works because…

  • reference_wrappers overload operator() so they can be called just like the function objects they refer to:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • …(un)like ordinary references, copying (and assigning) reference_wrappers just assigns the pointee.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>

Copying a reference wrapper is practically equivalent to copying a pointer, which is as cheap as it gets. All the function calls inherent in using it (e.g. the ones to operator()) should be just inlined as they are one-liners.

reference_wrappers are created via std::ref and std::cref:

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

The template argument specifies the type and cv-qualification of the object referred to; r2 refers to a const int and will only yield a reference to const int. Calls to reference wrappers with const functors in them will only call const member function operator()s.

Rvalue initializers are disallowed, as permitting them would do more harm than good. Since rvalues would be moved anyway (and with guaranteed copy elision even that's avoided partly), we don't improve the semantics; we can introduce dangling pointers though, as a reference wrapper does not extend the pointee's lifetime.

Library interaction

As mentioned before, one can instruct make_tuple to store a reference in the resulting tuple by passing the corresponding argument through a reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
// Type of t2 is tuple<int&>

Note that this slightly differs from forward_as_tuple: Here, rvalues as arguments are not allowed.

std::bind shows the same behavior: It won't copy the argument but store a reference if it is a reference_wrapper. Useful if that argument (or the functor!) need not be copied but stays in scope while the bind-functor is used.

Difference from ordinary pointers

  • There is no additional level of syntactical indirection. Pointers have to be dereferenced to obtain an lvalue to the object they refer to; reference_wrappers have an implicit conversion operator and can be called like the object they wrap.

    int i;
    int& ref = std::ref(i); // Okay
  • reference_wrappers, unlike pointers, don't have a null state. They have to be initialized with either a reference or another reference_wrapper.

    std::reference_wrapper<int> r; // Invalid
  • A similarity are the shallow copy semantics: Pointers and reference_wrappers can be reassigned.

Is there a legal way to move from gsl::not_nullT?

1&2: Ignoring the fact that elision would render the question moot on any compiler worth using, yes. Also ignoring the fact that unique_ptr cannot "leak".

3: No.

This has been the subject of some debate on the ISO C++ proposals mailing list. The general concept is that of a "destructive move", where the act of moving from an object and destroying it are performed in the same call. But this would have to be a language feature; there is no way in C++14 to tell whether a move constructor/assignment is being called such that the given object is certainly about to be destroyed.

Initializer list of reference wrappers

I think correctness is more important than avoiding verboseness. At least for serious projects.

→ Always use reference_wrapper<T> over T* when nullptr should not be valid.

If it's really too verbose, you can always define a type alias with a shorter name:

template<typename type> using ref = std::reference_wrapper<type>;

std::list<ref<const int>> objects_;

Or just a "tag" helper type to document the intent:

template<typename type> using not_null = type;

std::list<not_null<const int*>> objects_;

There's also the gsl::not_null<T*> class in the Guidelines Support Library. For a related Q&A, see gsl::not_null<T*> vs. std::reference_wrapper<T> vs. T&.

gsl::span - pointer to end

span is, like most of the C++ library, intended to be used with code that accepts template iterators. You can pass begin/end to algorithms or other containers and so forth.

Such APIs don't work nearly as well when you have to deal with an interface based purely on specific iterator pairs like pointers. It's no different from trying to get a pointer to the end of a std::vector; you're going to have to do pointer arithmetic sooner or later.

But if you're annoyed by warnings about pointer arithmetic, try using std::next(str.data(), str.size()) to compute the end pointer. The static analysis tool might complain about that, but it really shouldn't.



Related Topics



Leave a reply



Submit