4
\$\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
    \$\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\$ – Michaël Roy Jun 28 '17 at 20:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.