c++smart-pointersnull-pointerinvariantspreconditions

shared_ptr that cannot be null?


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?


Solution

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