17

Using a std::shared_ptr expresses shared ownership and optionality (with its possibility to be null).

I find myself in situations where I want to express shared ownership only in my code, and no optionality. When using a shared_ptr as a function parameter I have to let the function check that it is not null to be consistent/safe.

Passing a reference instead of course is an option in many cases, but I sometimes would also like to transfer the ownership, as it is possible with a shared_ptr.

Is there a class to replace shared_ptr without the possibility to be null, some convention to handle this problem, or does my question not make much sense?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Tobias Hermann
  • 9,936
  • 6
  • 61
  • 134
  • 9
    Pass a (const) reference instead. – O'Neil Jan 31 '17 at 10:32
  • http://stackoverflow.com/questions/11365149/missing-shared-ref – Simon Kraemer Jan 31 '17 at 10:35
  • 1
    ^ There's not many use cases for passing a `shared_ptr` by value. – M.M Jan 31 '17 at 10:36
  • Hmm, wouldn't it be possible to inherit/wrap `std::shared_ptr` and "hide" the default constructor and the `reset` function? – Simon Kraemer Jan 31 '17 at 10:40
  • Smart references are being proposed through [operator dot](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0416r1.pdf) and [delegation](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0352r0.pdf), but it seems like you really just want to pass a reference. – TartanLlama Jan 31 '17 at 10:57
  • Passing a reference of course is an option in many cases, but I sometimes would also like to pass the ownership, as it is possible with a `shared_ptr`. I updated my question accordingly. – Tobias Hermann Jan 31 '17 at 11:08
  • 5
    What problem are you trying to solve, really? If you feel comfortable assuming that your `shared_ptr` will never be null, then just document & assert this assumption and move on. You do test your code, right? – Lightness Races in Orbit Jan 31 '17 at 11:13
  • 4
    Of course I can document & assert. I'm talking about expressiveness and using the type system to prevent one kind of error at compile time. Additionally it would reduce the number of test cases needed per function by one, the case where a nullptr is passed. ;-) – Tobias Hermann Jan 31 '17 at 11:41
  • 1
    You may want to take a look at [gsl::not_null](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#i12-declare-a-pointer-that-must-not-be-null-as-not_null) from the [CppCoreGuidelines](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md) [GuidelineSupportLibrary](https://github.com/Microsoft/GSL). – Galik Jan 31 '17 at 11:47
  • How about something like this? http://coliru.stacked-crooked.com/a/c0272f8fb6186b8b – Simon Kraemer Jan 31 '17 at 17:14
  • @SimonKraemer Yes, this looks good. Thank you. I think something like that should be in the standard library. – Tobias Hermann Jan 31 '17 at 21:29

3 Answers3

8

You are asking for not_null wrapper class. Fortunately your issue is already addressed by C++ experts guideline and there are already example implementations - like this one. Search for not_null class template.

shinjin
  • 2,858
  • 5
  • 29
  • 44
PiotrNycz
  • 23,099
  • 7
  • 66
  • 112
  • In the talk: CppCon 2016: Neil MacIntosh “The Guideline Support Library: One Year Later", it is said that `not_null` is designed for raw pointers and not necessary fit for smart pointers. Do you have newer informations? –  Mar 10 '20 at 08:30
  • @generic_opto_guy it works for shared_ptr, so why not to use. I can accept that for unique_ptr not_null is not the best - because after "moving" it will be null - what is really not acceptable. – PiotrNycz Mar 10 '20 at 13:14
6

You could write a wrapper around std::shared_ptr that only allows creation from non-null:

#include <memory>
#include <cassert>

template <typename T>
class shared_reference
{
    std::shared_ptr<T> m_ptr;
    shared_reference(T* value) :m_ptr(value) { assert(value != nullptr);  }

public:
    shared_reference(const shared_reference&) = default;
    shared_reference(shared_reference&&) = default;
    ~shared_reference() = default;

    T* operator->() { return m_ptr.get(); }
    const T* operator->() const { return m_ptr.get(); }

    T& operator*() { return *m_ptr.get(); }
    const T& operator*() const { return *m_ptr.get(); }

    template <typename XT, typename...XTypes>
    friend shared_reference<XT> make_shared_reference(XTypes&&...args);

};


template <typename T, typename...Types>
shared_reference<T> make_shared_reference(Types&&...args)
{
    return shared_reference<T>(new T(std::forward<Types>(args)...));
}

Please note that operator= is missing yet. You should definitely add it.

You can use it like this:

#include <iostream>


using std::cout;
using std::endl;

struct test
{
    int m_x;

    test(int x)         :m_x(x)                 { cout << "test("<<m_x<<")" << endl; }
    test(const test& t) :m_x(t.m_x)             { cout << "test(const test& " << m_x << ")" << endl; }
    test(test&& t)      :m_x(std::move(t.m_x))  { cout << "test(test&& " << m_x << ")" << endl; }

    test& operator=(int x)          { m_x = x;                  cout << "test::operator=(" << m_x << ")" << endl; return *this;}
    test& operator=(const test& t)  { m_x = t.m_x;              cout << "test::operator=(const test& " << m_x << ")" << endl; return *this;}
    test& operator=(test&& t)       { m_x = std::move(t.m_x);   cout << "test::operator=(test&& " << m_x << ")" << endl; return *this;}

    ~test()             { cout << "~test(" << m_x << ")" << endl; }
};

#include <string>

int main() {

    {
        auto ref = make_shared_reference<test>(1);
        auto ref2 = ref;

        *ref2 = test(5);
    }
    {
        test o(2);
        auto ref = make_shared_reference<test>(std::move(o));
    }

    //Invalid case
    //{
    //  test& a = *(test*)nullptr;
    //  auto ref = make_shared_reference<test>(a);
    //}
}

Output:

test(1)
test(5)
test::operator=(test&& 5)
~test(5)
~test(5)
test(2)
test(test&& 2)
~test(2)
~test(2)

Example on Coliru

I hope I didn't forget anything that might result in undefined behaviour.

Simon Kraemer
  • 5,700
  • 1
  • 19
  • 49
  • Thanks. Exactly what I meant. But why does it need a custom `operator=`? The default one seems to work fine. – Tobias Hermann Feb 01 '17 at 14:53
  • Because of the [`rule-of-five`](http://en.cppreference.com/w/cpp/language/rule_of_three). – Simon Kraemer Feb 01 '17 at 15:05
  • 1
    OK, I understand the rule. Why is it needed in this case to declare the functions and default them? I think you mean [like that](http://coliru.stacked-crooked.com/a/500ba6ae0d885a31). Can we not simply leave them out completely [like this](http://coliru.stacked-crooked.com/a/0a003a4bbd369119). Or does this have negative effects I do not see yet? – Tobias Hermann Feb 01 '17 at 15:23
  • I think the move constructor should be declared as `= delete`, because moved-from `shared_reference` cannot be `nullptr`. – igagis Oct 12 '22 at 22:16
  • Check out my implementation https://github.com/cppfw/utki/blob/master/src/utki/shared_ref.hpp – igagis Oct 12 '22 at 22:22
1

After taking a look at GSL's not_null class, which calls std::terminate() instead of abort();

Here is how I achieved it:

template <typename T>
class NonNull : public std::shared_ptr<T> {
    typedef std::shared_ptr<T> super;
public:
    inline NonNull()
        : super(new T())
    {
        if ( ! super::get()) {
            abort(); // Out of memory.
        }
    }

    inline explicit NonNull(T *ptr)
        : super(ptr)
    {
        if ( ! super::get()) {
            abort(); // Input was null.
        }
    }
}

Basically, forces us to construct the class of T type.

Usage:

// Directly is a `std::shared_ptr` type:
NonNull<MyClass> myVariable;

// Unlike:
gsl::not_null<std::shared_ptr<MyClass > > myVariable;
Top-Master
  • 7,611
  • 5
  • 39
  • 71