Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserving the implicitness of construction in a policy-based class

Consider a policy-based smart pointer class Ptr with only one policy that will prevent dereferencing it in a NULL state (somehow). Let's consider 2 policies of this kind:

  • NotNull
  • NoChecking

Since the NotNull policy is more restrictive, we would like to allow implicit conversions from Ptr< T, NoChecking > to Ptr< T, NotNull >, but not in the opposite direction. That one has to be explicit for safety. Please take a look at the following implementation:

#include <iostream>
#include <type_traits>
#include <typeinfo>

struct NoChecking;
struct NotNull;

struct NoChecking{
  NoChecking()                    = default;
  NoChecking( const NoChecking&)  = default;

  explicit NoChecking( const NotNull& )
  { std::cout << "explicit conversion constructor of NoChecking" << std::endl; }

protected:
  ~NoChecking() {} //defaulting the destructor in GCC 4.8.1 makes it public somehow :o
};

struct NotNull{
  NotNull()                 = default;
  NotNull( const NotNull&)  = default;

  NotNull( const NoChecking& )
  { std::cout << "explicit conversion constructor of NotNull" << std::endl; }

protected:
  ~NotNull()    {}
};

template<
  typename T,
  class safety_policy
> class Ptr
: public safety_policy
{
private:
  T* pointee_;

public:
  template <
    typename f_T,
    class f_safety_policy
  > friend class Ptr;   //we need to access the pointee_ of other policies when converting
                        //so we befriend all specializations of Ptr

    //implicit conversion operator
  template<
    class target_safety
  > operator Ptr<T, target_safety>() const {
    std::cout << "implicit conversion operator of " << typeid( *this ).name() << std::endl;

    static_assert( std::is_convertible<const safety_policy&, const target_safety&>::value,  
                     //What is the condition to check? This requires constructibility
                  "Safety policy of *this is not implicitly convertible to target's safety policy." );

      //calls the explicit conversion constructor of the target type
    return Ptr< T, target_safety >( *this );
  }

    //explicit conversion constructor
  template<
    class target_safety
  > explicit Ptr( const Ptr<T, target_safety>& other )
  : safety_policy( other ),  //this is an explicit constructor call and will call explicit constructors when we make Ptr() constructor implicit!
    pointee_( other.pointee_ )
  { std::cout << "explicit Ptr constructor of " << typeid( *this ).name() << std::endl; }

  Ptr() = default;
};

  //also binds to temporaries from conversion operators
void test_noChecking( const Ptr< int, NoChecking >& )
{  }

void test_notNull( const Ptr< int, NotNull >& )
{  }

int main()
{
  Ptr< int, NotNull    > notNullPtr;                //enforcing not null value not implemented for clarity
  Ptr< int, NoChecking > fastPtr( notNullPtr );     //OK - calling explicit constructor and NotNull is explicitly convertible to NoChecking

  test_notNull    ( fastPtr    );    //should be OK    - NoChecking is implictly convertible to NotNull
  test_noChecking ( notNullPtr );    //should be ERROR - NotNull is explicitly convertible to NoChecking

  return 0;
}

Live example

The code above fails when implicitly converting in both directions, which means that std::is_convertible fails even though the classes have compatible constructors. The problems are:

  1. Constructor overloads cannot differ simply by explicit keyword, so we need an explicit constructor and implicit conversion operator (or vice versa) in the host class.
  2. Explicit constructor is better, because any constructor will call explicit constructors from initialization list even if is implicit itself.
  3. Implicit conversion operator cannot create objects of policy type because their destructor is protected. This is why std::is_convertible fails when it shouldn't, and this is also why we can't use something like boost::implicit_cast< const target_policy& >( *this ) in the conversion operator, as it would create a temporary policy object, which is forbidden.

As for the obvious solutions that are not optimal in my opinion:

  1. Make the policy destructor public - and risk UB when casting the Ptr* to policy* and deleting it? This unlikely in the example provided, but is possible in real-world application.
  2. Make the destructor public and use protected inheritance - I need the enriched interface the public inheritance provides.

The question is:

Is there a static test for existence of implicit constructor from one type to another that does not create objects of these types?

Or alternatively:

How do I preserve the information of implicit construction when calling the policies' constructors from the host class' constructor?


EDIT:

I just realized that the second question can be easily answered with a private, implicit-flagged constructor like this:

#include <iostream>
#include <type_traits>
#include <typeinfo>

struct implicit_flag {};

struct NoChecking;
struct NotNull;

struct NoChecking{
  NoChecking()                    = default;
  NoChecking( const NoChecking&)  = default;

protected:
  explicit NoChecking( const NotNull& )
  { std::cout << "explicit conversion constructor of NoChecking" << std::endl; }


  ~NoChecking() {} //defaulting the destructor in GCC 4.8.1 makes it public somehow :o
};

struct NotNull{
  NotNull()                 = default;
  NotNull( const NotNull&)  = default;

protected:
  NotNull( implicit_flag, const NoChecking& )
  { std::cout << "explicit conversion constructor of NotNull" << std::endl; }

  ~NotNull()    {}
};

template<
  typename T,
  class safety_policy
> class Ptr
: public safety_policy
{
private:
  T* pointee_;

public:
  template <
    typename f_T,
    class f_safety_policy
  > friend class Ptr;   //we need to access the pointee_ of other policies when converting
                        //so we befriend all specializations of Ptr

    //implicit conversion operator
  template<
    class target_safety
  > operator Ptr<T, target_safety>() const {
    std::cout << "implicit conversion operator of " << typeid( *this ).name() << std::endl;

    /*static_assert( std::is_convertible<const safety_policy&, const target_safety&>::value,  //What is the condition to check? This requires constructibility
                  "Safety policy of *this is not implicitly convertible to target's safety policy." );*/

      //calls the explicit conversion constructor of the target type
    return Ptr< T, target_safety >( implicit_flag(), *this );
  }

    //explicit conversion constructor
  template<
    class target_safety
  > explicit Ptr( const Ptr<T, target_safety>& other )
  : safety_policy( other ),  //this is an explicit constructor call and will not preserve the implicity of conversion!
    pointee_( other.pointee_ )
  { std::cout << "explicit Ptr constructor of " << typeid( *this ).name() << std::endl; }

private:

    //internal implicit-flagged constructor caller that is called from implicit conversion operator
  template<
    class target_safety
  > Ptr( implicit_flag implicit, const Ptr<T, target_safety>& other )
  : safety_policy( implicit, other ),  //this constructor is required in the policy
    pointee_( other.pointee_ )
  { std::cout << "explicit Ptr constructor of " << typeid( *this ).name() << std::endl; }

public:
  Ptr() = default;
};

  //also binds to temporaries from conversion operators
void test_noChecking( const Ptr< int, NoChecking >& )
{  }

void test_notNull( const Ptr< int, NotNull >& )
{  }

int main()
{
  Ptr< int, NotNull    > notNullPtr;                //enforcing not null value not implemented for clarity
  Ptr< int, NoChecking > fastPtr( notNullPtr );     //OK - calling explicit constructor and NotNull is explicitly convertible to NoChecking

  test_notNull    ( fastPtr    );    //should be OK    - NoChecking is implictly convertible to NotNull
  test_noChecking ( notNullPtr );    //should be ERROR - NotNull is explicitly convertible to NoChecking

  return 0;
}

The errors however are not very readable and we introduce an unnecessary requirement to the policies, so an answer to the first question is more preferable.

like image 238
tsuki Avatar asked Aug 27 '14 10:08

tsuki


1 Answers

See the "perfect initialization" approach taken by N4064 for std::pair and std::tuple which involves testing std::is_constructible<T, U>::value and std::is_convertible<U, T>::value

If both are true, there is an implicit conversion, if only the first is true the conversion is explicit.

The solution is to define two overloads for the constructor, one that is implicit and one explicit, and use SFINAE so that at most one overload is viable.

like image 116
Jonathan Wakely Avatar answered Oct 21 '22 08:10

Jonathan Wakely