shared_ptr that cannot be null?
Asked Answered
T

3

18

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?

Tyrosine answered 31/1, 2017 at 10:30 Comment(11)
Pass a (const) reference instead.Mychal
https://mcmap.net/q/741820/-missing-shared_refBucher
^ There's not many use cases for passing a shared_ptr by value.Osswald
Hmm, wouldn't it be possible to inherit/wrap std::shared_ptr and "hide" the default constructor and the reset function?Bucher
Smart references are being proposed through operator dot and delegation, but it seems like you really just want to pass a reference.Palladio
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.Tyrosine
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?Harmonie
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. ;-)Tyrosine
You may want to take a look at gsl::not_null from the CppCoreGuidelines GuidelineSupportLibrary.Vevay
How about something like this? coliru.stacked-crooked.com/a/c0272f8fb6186b8bBucher
@SimonKraemer Yes, this looks good. Thank you. I think something like that should be in the standard library.Tyrosine
B
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.

Bucher answered 1/2, 2017 at 9:41 Comment(5)
Thanks. Exactly what I meant. But why does it need a custom operator=? The default one seems to work fine.Tyrosine
Because of the rule-of-five.Bucher
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. Can we not simply leave them out completely like this. Or does this have negative effects I do not see yet?Tyrosine
I think the move constructor should be declared as = delete, because moved-from shared_reference cannot be nullptr.Kosygin
Check out my implementation github.com/cppfw/utki/blob/master/src/utki/shared_ref.hppKosygin
G
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.

Greenlaw answered 31/1, 2017 at 11:42 Comment(2)
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?Malinowski
@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.Greenlaw
B
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.

Bucher answered 1/2, 2017 at 9:41 Comment(5)
Thanks. Exactly what I meant. But why does it need a custom operator=? The default one seems to work fine.Tyrosine
Because of the rule-of-five.Bucher
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. Can we not simply leave them out completely like this. Or does this have negative effects I do not see yet?Tyrosine
I think the move constructor should be declared as = delete, because moved-from shared_reference cannot be nullptr.Kosygin
Check out my implementation github.com/cppfw/utki/blob/master/src/utki/shared_ref.hppKosygin
F
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;
Fritts answered 6/7, 2021 at 0:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.