6
\$\begingroup\$

Edit: I have refined the concept a bit further, and my new implementation can be found here


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, so with some tinkering I basically rebuilt optional with support for undefined types, as such:

#ifndef __NULLABLE_
#define __NULLABLE_
#include <memory>       // std::unique_ptr
#include <functional>   // std::function
#include <utility>      // std::pair
#include <stdexcept>    // std::logic_error

namespace util {

namespace detail {
    namespace Nullable {

        template <typename T> using data_type = std::unique_ptr<T, void( *)( T* )>;
        template <typename T> using copy_fx_type = std::function<data_type<T>( const data_type<T>& )>;

        // Move T to data type
        //  Universal reference version:  having an overload seems to cause this to become a rvalue-reference version only, so we'll only support rvalues
        template <typename T> 
        inline data_type<T> make_data( T&& what ) {
            return{ new T( std::forward<T>( what ) ), []( T* ptr ) { if ( ptr ) delete ptr; } };
        }

        // create empty data type
        template <typename T>
        inline data_type<T> make_data() {
            return{ nullptr, []( T* ) {} }; // no-op deleter
        }

        template <typename T> using _state_base_type = std::pair<data_type<T>, copy_fx_type<T>>;

        // internal state for Nullable<T>
        template <typename T>
        struct state 
            : _state_base_type<T> {

            using base_type = _state_base_type<T>;

            state( data_type<T>&& data, copy_fx_type<T>&& copy_fx )
                : base_type{ std::move( data ), std::move( copy_fx ) }
            {}

            state()
                : state{ make_data<T>(), copy_fx_type<T>{} }    // delegate
            {}

            state( T&& what, copy_fx_type<T>&& copy_fx )
                : state{ make_data<T>( std::move( what ) ), std::move( copy_fx ) }  // delegate
            {}

            state( state&& ) = default;
            state& operator=( state&& ) = default;

            state( const state& rhs ) { *this = rhs; }
            state& operator=( const state& rhs ) {
                if ( this != &rhs )
                    *this = rhs.clone();
                return *this;
            }

            // clones current state via stored data copy mechanism
            state clone() const {
                if ( this->first )
                    return{ this->second( this->first ), copy_fx_type<T>{ this->second } };
                return{};
            }

        };  // state

        template <typename T> using state_type = state<T>;

    }   //  Nullable
}   // detail

// Like boost/std::optional with undefined type support
template <typename T>
struct Nullable {
private:
    using _value_type = T;
    using _state_type = detail::Nullable::state_type<T>;
    using _data_type = typename _state_type::first_type;
    using _fx_copy_type = typename _state_type::second_type;

    _state_type _state;

    // Get *
    _value_type* _get() { return this->_state.first.get(); }

    // Get T*
    const _value_type* _get() const { return this->_state.first.get(); }

    // Create state from value
    static _state_type _create_state( _value_type what ) {
        return {
            std::move( what )
            , []( const _data_type& what ) -> _data_type {
                if ( what )
                    return detail::Nullable::make_data<_value_type>( _value_type{*what} );  // call copy ctor of _value_type here
                return detail::Nullable::make_data<_value_type>();
            }
        };
    }   // _create_state

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

    // Construct default nullable
    Nullable() = default;

    // Construct default nullable
    Nullable( nullopt_t ) : Nullable{} {}

    // Construct nullable with value_type&&
    Nullable( value_type&& what )
        : _state{ _create_state( std::move( what ) ) }
    {}

    // Construct by forwarding args to value_type constructor
    //  todo:  need to disambiguate with value_type ctor.   for now, callers may use make_nullable<T>(...)
    /*
    template<class Arg0, class... Args>
    Nullable( Arg0&& arg0, Args&&... args )
        : Nullable{ value_type( std::forward<Arg0>( arg0 ), std::forward<Args>( args )... ) }
    {}
    */

    Nullable( Nullable&& ) = default;
    Nullable& operator=( Nullable&& ) = default;

    Nullable( const Nullable& other ) { if ( this != &other ) *this = other; }
    // Nullable( const Nullable& ) = default;   // msvc15 won't compile due to trying to default construct unique_ptr in state...not sure why default state ctor isn't called

    Nullable& operator=( const Nullable& ) = default;

    // assign nullopt_t
    Nullable& operator=( nullopt_t ) { 
        this->reset(); 
        return *this; 
    }

    // assign value
    Nullable& operator=( value_type what ) { 
        this->_state = _create_state( std::move( what ) );
        return *this; 
    }

    // compare to nullopt_t
    bool operator==( nullopt_t ) const { return !this->has_value(); }

    // compare to nullopt_t
    bool operator!=( nullopt_t ) const { return !( *this == nullptr ); };

    // 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" };
    }

    //Returns the contained value if *this has a value, otherwise returns default_value
    template<class U>
    const value_type& value_or( const U& default_value ) const {
        return bool( *this ) ? **this : static_cast<const value_type&>( default_value );
    }

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

    // emplaces value into nullable, returns reference to value
    template<class... Args>
    T& emplace( Args&&... args ) {
        *this = { value_type( std::forward<Args>( args )... ) };
        return this->value();
    }
};  // Nullable

// make_nullable
template<class T>
constexpr Nullable<std::decay_t<T>> make_nullable( T&& value ) { return{ std::forward<T>( value ) }; }

// make_nullable
template<class T, class... Args>
constexpr Nullable<T> make_nullable( Args&&... args ) { return make_nullable( T( std::forward<Args>( args )... ) ); }

}   // ns
#endif

Compiles/runs fine with msvc15, haven't tested others. Basically I'm storing the pointer in a unique_ptr and storing/using a lambda to call the copy ctor when T is actually defined.

However, I hate reinventing the wheel and perhaps I'm overlooking an alternate solution, aside from fully defining each class in order to use boost/std::optional. Also, any feedback on this class is appreciated. I know I'm missing most comparison operators and whatnot, but I'm not trying to replace optional...I just want value semantics for my undefined types, and a clone of optional is the nearest logical candidate afaict.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I don't understand your motivation as stated. I've just tried to compile the code below with:

gcc  -std=c++11 -Wall -Wpedantic

And it compiles just fine. Default constructor, copy constructor and all... No warnings, either. Something in your declaration is preventing you to use the default constructors, but it's not because A is not fully defined.

#include <vector>

struct A;

struct B {
  A* a;
};

int main()
{
  B x = B(), y{nullptr};
  B z(x);

  x = y;

  std::vector<B> v;

  return v.size();
}
\$\endgroup\$
3
  • \$\begingroup\$ Sorry if I wasn't clear. The problem is that I want value semantics for A without having to define A. In this case, I want each B to have its own A. Placing a A* in B will only copy the pointer with each move/copy of B. Hope that makes sense. \$\endgroup\$
    – Tom
    Jun 28, 2017 at 21:04
  • \$\begingroup\$ It kind of does, But since that can be easily obtained by defining the constructors. I find it s a bit misleading. I'm not saying that there is no use case scenario for Nullable, though. \$\endgroup\$ Jun 28, 2017 at 23:06
  • \$\begingroup\$ I agree the name is misleading, and that's part of the reason for the refactor and edit. Even still, I'm not sure how to succinctly describe what the revised class does. Any suggestions? \$\endgroup\$
    – Tom
    Jun 28, 2017 at 23:34

Your Answer

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

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