Preserving the implicitness of construction in a policy-based class
Asked Answered
A

2

6

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.

Amends answered 27/8, 2014 at 10:36 Comment(0)
C
4

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.

Cronk answered 27/8, 2014 at 11:30 Comment(1)
std::is_constructible< NoChecking, const NotNull&>::value and vice versa are false. It is impossible to construct an object of policy type because of private destructor, so I'm not sure how this helps. Fascinating trick though. I'm sure I'll be using it somewhere else.Amends
A
0

Well, that took me some time to realize, but if the problem lies in the fact, that we can't create objects of type policy because its destructor is protected, then why don't we host it in a temporary forwarding class?

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;

    struct target_host : target_safety { using target_safety::target_safety; };

    static_assert( std::is_convertible<Ptr, target_host>::value,
                     //Now this works, because target_host is constructible!
                  "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 );
  }

Live demonstration

The trick is to forward the constructors of target_policy to target_host, so it can be constructed from arguments that target_policy can. Since Ptr derives from safety_policy it can be implicitly converted to (const) safety_policy&(&). This means that tesing conversion Ptr -> target_host is equivalent to testing construction target_host::target_safety(safety_policy).


Using the perfect initialization trick provided by Jonathan Wakely in conjunction with temporary policy hosting we can solve it in the following way:

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

template< typename Policy >
struct policy_host_
: Policy
{
  using Policy::Policy;
};

template< typename Source, typename Target >
struct is_implicitly_convertible
: std::integral_constant<
    bool
  , std::is_constructible< policy_host_<Target>,  policy_host_<Source> >::value &&
    std::is_convertible<   policy_host_<Source>,policy_host_<Target>   >::value
  >
{  };

template< typename Source, typename Target >
struct is_explicitly_convertible
: std::integral_constant<
    bool
  , std::is_constructible< policy_host_<Target>,  policy_host_<Source> >::value &&
    !std::is_convertible<  policy_host_<Source>,policy_host_<Target>   >::value
  >
{  };

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

  template<
    class target_safety,
    typename std::enable_if<
      is_implicitly_convertible< target_safety, safety_policy >::value
    , bool>::type = false
  > Ptr( const Ptr<T, target_safety>& other )
  : safety_policy( other ),
    pointee_( other.pointee_ )
  { std::cout << "implicit Ptr constructor of " << typeid( *this ).name() << std::endl; }

  template<
    class target_safety,
    typename std::enable_if<
      is_explicitly_convertible< target_safety, safety_policy >::value
    , bool>::type = false
  > 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; }

  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 demo

Amends answered 27/8, 2014 at 15:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.