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.