5
\$\begingroup\$

Edit: final revision here


A couple days ago I posted a similar question here. Since then, I have refined the implementation a bit further, as the solution I had previously posited was a bit off target. To repeat my problem statement:

Motivation: I found myself with class members of forward declared types in a header, such as:

struct A;

struct B {
    A* a;
}

// B.cpp
include "A.h"
// do stuff with a real A

The problem is that this prevented me from using the default move/copy ctors of B while preserving value semantics of a; even though a is defined as a pointer just to get around the undefined issue, I really want a to be a value. However, I do not want to fully define A in B's header due to the import of various headers/etc that I would like to keep hidden. Unfortunately, I am unable to use boost/std::optional with an undefined type.

My revised solution is focused on preserving value semantics for a type which may or may not be defined at the time of declaration. After refactoring my previous Nullable<T> into a deep_ptr<T>, it becomes easy to provide a nullable/optional<T> wrapper around it as I provide below.

The features of the class in its current state are undefined type support (of course), unique_ptr compatibility, deep copying by default, and custom copy and/or delete functions via functors or stateless lambdas. I believe these are the minimum features needed to solve my original problem and provide a solid base from which to build if needed. Tested on msvc15, clean per visual leak detector.

#ifndef _DEEP_PTR_
#define _DEEP_PTR_

#include <cassert>      // assert
#include <memory>       // std::unique_ptr

namespace ptr {

namespace detail {
    // dispatches delete to functor DeleteOp
    template <typename T, typename DeleteOp>
    struct delete_dispatcher {
        static void op( T* ptr ) {
            DeleteOp{}( ptr );
        }
    };  // delete_dispatcher

    // dispatches to copy functor Op
    template <typename T, typename Op>
    struct copy_dispatcher {
        static auto op( const T& what ) {
            return Op{}( what );
        }
    };  // copy_dispatcher

    // default copy functor
    template <typename T>
    struct default_copy {
        auto operator()( const T& what ) const {
            return new std::decay_t<T>( what );
        }
    };  // default_copy

    template <typename T> using deep_ptr_base = std::unique_ptr<T, void( *)( T* )>;
}   // detail ns

// deep-copying, value semantic wrapper around std::unique_ptr
//  DeleteOp functor or lambda signature:   void operator()(T*)
//  CopyOp functor or lambda signature:     T* operator()( const T& )
//  Stateless lambdas can be provided via constructors and those will override defined types
template <typename T, typename DeleteOp = std::default_delete<T>, typename CopyOp = detail::default_copy<T>>
struct deep_ptr
    : private detail::deep_ptr_base<T> {

    using base = detail::deep_ptr_base<T>;

    // import from unique_ptr
    using base::element_type;
    using base::get;
    using base::release;
    using base::reset;
    using base::swap;
    using base::operator bool;
    using base::operator->;
    using base::operator*;
    using base::pointer;

    using deleter_type = DeleteOp;
    using copier_type = CopyOp;

    // copy function pointer
    using copy_fx_type = element_type*( *)( const element_type& );

    // returns function pointer to copy dispatcher
    static constexpr auto default_copier() {
        return &detail::copy_dispatcher<element_type, copier_type>::op;
    }

    // returns function pointer to delete dispatcher
    static constexpr auto default_deleter() {
        return &detail::delete_dispatcher<element_type, deleter_type>::op;
    }

    // construct empty ptr
    deep_ptr( std::nullptr_t = nullptr )
        : base{ nullptr, []( pointer ) {
            assert( false );    // should never hit this
        } }
        , _copy_fx{}
    {}

    // construct with pointer, deleter
    //  Deleter and Copier parameters will accept lambda or functor
    //  Any provided lambda/functor will override the types provided in deep_ptr definition
    template <typename Deleter, typename Copier>
    deep_ptr( pointer px, Deleter&& dx, Copier&& cx )
        : base{ px, std::forward<Deleter>( dx ) }
        , _copy_fx{ std::forward<Copier>( cx ) }
    {}

    // construct with pointer, deleter
    //  Deleter parameter will accept lambda or functor
    template <typename Deleter>
    deep_ptr( pointer px, Deleter&& dx )
        : deep_ptr{
        px
        , std::forward<Deleter>( dx )
        , default_copier()
    }
    {}

    // construct with pointer, default delete/copy 
    deep_ptr( pointer px )
        : deep_ptr{
        px
        , default_deleter()
        , default_copier()
    }   // delegate
    {}

    // construct from unique_ptr<T, Dx>
    template <typename Dx>
    deep_ptr( std::unique_ptr<element_type, Dx>&& what )
        : deep_ptr{
            what.release()
            , &detail::delete_dispatcher<element_type, Dx>::op
        }
    {}

    // default move ctor
    deep_ptr( deep_ptr&& ) = default;   

    // default move assign
    deep_ptr& operator=( deep_ptr&& ) = default;    

    // deep copy constructor
    deep_ptr( const deep_ptr& rhs )
        : deep_ptr{} {
        *this = rhs;
    }

    // deep copy assignment operator
    deep_ptr& operator=( const deep_ptr& rhs ) {

        if ( this == &rhs )
            return *this;

        // execute copy op
        if ( rhs ) {
            assert( rhs.get_copier() != nullptr );
            *this = { 
                rhs.get_copier()( *rhs.get() )
                , rhs.get_deleter()
                , rhs.get_copier() 
            };  // copier expected to return T*, which can be null
        }
        else
            *this = nullptr;

        return *this;
    }   // op=(const& deep_ptr)

    // returns copier, analagous to unique_ptr get_deleter()
    copy_fx_type get_copier() const {
        return this->_copy_fx;
    }

private:
    // copy function pointer
    copy_fx_type _copy_fx;

};  // deep_ptr

}   // ns
#endif

Tests/usage:

using namespace ptr;

// test undefined in MyStruct from deep_ptr.h
MyStruct s{ new undefined{7} };

assert( s.val->foo == 7 );

// test default move with deep_ptr<undefined>
auto s1 = std::move( s );
assert( s1.val->foo == 7 );
assert( !s.val );

// test default copy with deep_ptr<undefined>
auto s2 = s1;
assert( s2.val->foo == 7 );
assert( s1.val->foo == 7 );


struct A;
struct B {
    int foo;
    int bar;
};

struct C : B {
    int baz;
};

{
    // undefined type
    deep_ptr<A> undefined{};
    assert( undefined.get() == nullptr );
    assert( !undefined );

    // define A
    struct A { int blah; };
    deep_ptr<A> a{ new A{3} };
    assert( ( *a ).blah == 3 );
    assert( (bool)a );
}

{
    deep_ptr<B> defined{ new B{1,2} };
    auto defined2 = defined;    // copy op=
    assert( defined2->foo == 1 );
    assert( defined2->bar == 2 );
    deep_ptr<B> defined3{ defined2 };   // copy ctor
    assert( defined3->foo == 1 );
    assert( defined3->bar == 2 );

    deep_ptr<B> defined4{ std::move( defined2 ) };// move ctor
    assert( defined4->foo == 1 );
    assert( defined4->bar == 2 );
    assert( !defined2 );

    auto defined5 = std::move( defined3 );  // move op
    assert( defined5->foo == 1 );
    assert( defined5->bar == 2 );
    assert( !defined3 );

    // nullptr_t assignment
    defined5 = nullptr;// reset
    assert( !defined5 );
}

// deep_ptr with functor deleter 
static bool mydeleter_test_passed = false;
{
    struct mydeleter {
        void operator()( B* ptr ) const {
            delete ptr;
            mydeleter_test_passed = true;
        }
    };

    deep_ptr<B, mydeleter> functor{ new B{ 1,2 } };
}
assert( mydeleter_test_passed );

// deep_ptr with stateless lambda deleter
static bool lambda_deleter_test_passed = false;
{
    deep_ptr<B> lambda{ new B{ 1,2 }, []( B* ptr ) {
        delete ptr;
        lambda_deleter_test_passed = true;
    } };
}
assert( lambda_deleter_test_passed );

// deep_ptr with copy functor
static bool mycopier_test_passed = false;
{
    struct mycopier {
        auto operator()( const B& what ) const {
            mycopier_test_passed = true;
            return new B{ what };
        }
    };
    deep_ptr<B, std::default_delete<B>, mycopier> copy_functor{ new B{ 1,2 } };
    auto mycopy = copy_functor;
    assert( mycopy->foo == 1 );
    assert( mycopy->bar == 2 );
}
assert( mycopier_test_passed );

// deep_ptr with stateless lambda copier
static bool lambda_copier_test_passed = false;
{
    deep_ptr<B> lambda{
        new B{ 1,2 }
        , deep_ptr<B>::default_deleter()
        , []( const B& what ) {
            lambda_copier_test_passed = true;
            return new B{ what };
            }
    };
    auto copy = lambda;// try custom copier
}
assert( lambda_copier_test_passed );

// derived type custom copier
{
    C c{};
    c.foo = 1;
    c.bar = 2;
    c.baz = 3;

    deep_ptr<B> lambda{
        new C{c}
        , deep_ptr<B>::default_deleter()
        , []( const B& what ) -> B* {   // must return B*
            return new C{ static_cast<const C&>( what ) };  // cast what to const C&, copy construct a C
        }
    };
    auto copy = lambda;// execute
    assert( static_cast<C&>( *copy ).baz == 3 );
}

// construct from std::unique_ptr<T, Deleter>
static bool uptr_deleter_test_passed = false;
{
    struct my_deleter {
        void operator()( int* ptr ) { 
            delete ptr; 
            uptr_deleter_test_passed = true;
        }
    };

    deep_ptr<int> deepint{};
    {
        std::unique_ptr<int, my_deleter> myint{ new int( 5 ) };
        deepint = std::move( myint );   // op=(unique_ptr<T, Dx>&&)

        assert( !myint );
        assert( *deepint == 5 );
    }
    assert( !uptr_deleter_test_passed );    // deleter should not have been called yet
}
assert( uptr_deleter_test_passed );

Finally, my Nullable<T> class reimplemented using deep_ptr<T>:

// Example polymorphic optional using deep_ptr with undefined type support
template <typename T, typename Copier>
struct Nullable {
private:
    using _value_type = T;
    using _data_type = ptr::deep_ptr<_value_type, std::default_delete<_value_type>, Copier>;

    _data_type _data;

    // Get T*
    auto _get() { return this->_data.get(); }

    // Get const T*
    auto _get() const { return this->_data.get(); }

public:
    using value_type = _value_type;
    using nullopt_t = std::nullptr_t;

    Nullable() = default;

    // Construct with value_type
    Nullable( value_type what )
        : _data{ new value_type{ std::move( what ) } }
    {}

    // Construct with derived type
    //  todo: some SFINAE on Derived, maybe
    template <typename Derived>
    Nullable( Derived d )
        : _data{ new Derived{std::move( d )} }
    {}

    // returns stored value, UB if none
    value_type& operator*() { return *this->_get(); }
    const value_type& operator*() const { return *this->_get(); }

    // returns pointer to stored value, UB if none
    const value_type* operator->() const { return this->_get(); }
    // returns pointer to stored value, UB if none
    value_type* operator->() { return this->_get(); }

    // returns flag if this has value
    explicit constexpr operator bool() const {
        return this->_get() != nullptr;
    }

    // returns flag if this has value
    constexpr bool has_value() const { return bool( *this ); }

    // returns T&, or throws if value undefined
    value_type& value() {
        if ( auto ptr = this->_get() )
            return *ptr;
        throw std::logic_error{ "Value not set" };
    }

    // returns const T&, or throws if value undefined
    const value_type& value() const {
        if ( auto ptr = this->_get() )
            return *ptr;
        throw std::logic_error{ "Value not set" };
    }

    // resets value
    void reset() { this->_data = {}; }
};  // Nullable

Tests/usage of Nullable<T>

// example usage:  Polymorphic Nullable
struct base {
    int foo;
    base( int foo_ ) :foo{ foo_ } {}
    virtual base* clone() const { return new base{ *this }; }
};

struct derived : base {
    int bar;
    derived( int foo_, int bar_ ) : base{ foo_ } , bar{ bar_ } {}
    base* clone() const override { return new derived{ *this }; }
};

struct PolymorphicCopier {
    base* operator()( const base& what ) const {
        return what.clone();
    }
};

::Nullable<base, PolymorphicCopier> myopt{};

// test with base class
myopt = base{ 1 };
assert( myopt.value().foo == 1 );

auto myopt2 = myopt;    // copy
assert( myopt2.value().foo == 1 );

// test with derived class in Nullable<base>
myopt = derived{ 8,9 };
assert( static_cast<const derived&>( myopt.value() ).bar == 9 );
auto myopt3 = myopt;    // copy
assert( static_cast<const derived&>( myopt3.value() ).bar == 9 );   // look ma, no slicing!
auto myopt4 = std::move( myopt3 );  // move
assert( !myopt3.has_value() );
assert( static_cast<const derived&>( myopt4.value() ).bar == 9 );   // look ma, no slicing!

Thoughts and feedback appreciated. Off the top of my head, I'm not sure if there's a better way to handle the dispatching of the functors/lambdas in deep_ptr, as I'm trying to avoid holding a std::function due to size bloat, and I'm expecting the compiler to optimize the lambdas/functors to pointers. Or, in keeping with the "you don't pay for what you don't use" paradigm of C++, some way to minimize the size of deep_ptr in the case of using the default delete/copy handlers. Seems like I would need to store two function pointers at minimum, but maybe there's a better way.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ The not fully defined A* does not prevent default constructors to be defined in any way. But you will need to define B's destructor's body in B.cpp. \$\endgroup\$ Commented Jun 28, 2017 at 20:42

1 Answer 1

1
\$\begingroup\$

It would have been helpful to have the unit tests as part of this review. That helps reviewers see what use-cases have been provided for and can help identify gaps in testing. Really good tests also show what's expected to be invalid (using the Detection Idiom to verify rejection).


In the template, we need to tell the compiler which base identifiers represent types, using typename:

    using typename base::element_type;
    using base::get;
    using base::release;
    using base::reset;
    using base::swap;
    using base::operator bool;
    using base::operator->;
    using base::operator*;
    using typename base::pointer;

With that change, I can compile cleanly using GCC.


I think that this constructor ought to be explicit:

    deep_ptr( pointer px )

Just like std::unique_ptr, we don't want to accidentally take ownership of an existing object. That might lead to unexpected early deletion.


This smart pointer is less flexible than std::unique_ptr in terms of deleter. We only accept function pointers, but I think we should accept any function object, by inheriting from std::unique_ptr<T, DeleteOp>, and do away with the despatchers.


Since copying a deep pointer has value semantics, I think it's reasonable to create a pointer-to-mutable from a pointer-to-const, like this:

    const int *a = new int{6};
    ptr::deep_ptr<const int> p = &a;
    ptr::deep_ptr<int> q = p;
    assert(p.get() != q.get());

That's ill-formed, which is unfortunate.


A constructor that copies from a const T& would be useful, particularly when writing constructors:

struct B {
    ptr::deep_ptr<A> a;
    B(const A& a) : a{ptr::deep_ptr<A>{a}} {} // INVALID!
};

Rather than a constructor, perhaps a function to copy a value into a deep pointer?

    template<typename T,
             typename Deleter = std::default_delete<T>,
             typename Copier = detail::default_copy<T>>
    deep_ptr<T, Deleter, Copier>
    deep_copy(const T& object, Deleter = {}, Copier c = {})
    {
        return {c(object),
                // this would be easier if we could simply pass the
                // function objects
                &detail::delete_dispatcher<T, Deleter>::op,
                &detail::copy_dispatcher<T, Copier>::op};
    }

That permits

    B(const A& a) : a{ptr::deep_copy(a)} {}

The assignment operator allocates memory (by creating a new deep pointer), but that shouldn't be necessary when T is copy-assignable. Avoiding allocation for this common case will improve performance and eliminate a possible source of std::bad_alloc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.