在基于策略的类中保留构造的隐含性

时间:2014-08-27 10:36:29

标签: c++ c++11 constructor implicit-conversion policy-based-design

考虑一个基于策略的智能指针类Ptr,只有一个策略可以防止在NULL状态下解除引用它(不知何故)。让我们考虑两种此类政策:

  • NotNull
  • NoChecking

由于NotNull政策限制性更强,我们希望允许从Ptr< T, NoChecking >Ptr< T, NotNull >进行隐式转换,但不允许相反方向。那一个必须明确安全。请看一下以下实现:

#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

上述代码在双向隐式转换时失败,这意味着即使类具有兼容的构造函数,std::is_convertible也会失败。问题是:

  1. 构造函数重载只能通过显式关键字来区分,因此我们需要在宿主类中使用显式构造函数和隐式转换运算符(反之亦然)。
  2. 显式构造函数更好,因为任何构造函数都会从初始化列表中调用显式构造函数,即使它本身是隐式的。
  3. 隐式转换运算符无法创建策略类型的对象,因为它们的析构函数受到保护。这就是为什么std::is_convertible在它不应该失败时失败的原因,这也是我们在转换运算符中不能使用boost::implicit_cast< const target_policy& >( *this )之类的原因,因为它会创建一个临时策略对象,这是被禁止的。
  4. 至于我认为不是最优的明显解决方案:

    1. 使策略析构函数公开 - 并在将Ptr *转换为策略*并删除它时冒险UB?这在提供的示例中不太可能,但在实际应用中是可能的。
    2. 使析构函数公开并使用受保护的继承 - 我需要公共继承提供的丰富接口。
    3. 问题是:

      是否存在从一种类型到另一种类型的隐式构造函数的存在的静态测试,它不会创建这些类型的对象?

      或者:

      如何在调用策略时保留隐式构造的信息&#39;来自主持人类的构造函数&#39;构造吗


      修改

      我刚刚意识到第二个问题可以通过这样的私有隐式标记构造函数轻松回答:

      #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;
      }
      

      然而,错误的可读性并不高,我们对政策提出了不必要的要求,因此第一个问题的答案更为可取。

2 个答案:

答案 0 :(得分:4)

请参阅N4064针对std::pairstd::tuple采取的“完美初始化”方法,该方法涉及测试std::is_constructible<T, U>::valuestd::is_convertible<U, T>::value

如果两者都为真,则存在隐式转换,如果仅第一个为真,则转换是显式的。

解决方案是为构造函数定义两个重载,一个是隐式的,一个是explicit,并使用SFINAE,这样最多只有一个重载是可行的。

答案 1 :(得分:0)

嗯,这花了我一些时间才意识到,但如果问题在于,我们无法创建policy类型的对象,因为它的析构函数是protected,那么为什么要'我们将它托管在临时转发类中吗?

Ptr隐式转换运算符:

  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

诀窍是将target_policy的构造函数转发给target_host,因此可以使用target_policy可以构造的参数构造它。由于Ptr派生自safety_policy,因此可以将其隐式转换为(const) safety_policy&(&)。这意味着测试转换Ptr -> target_host等同于测试构造target_host::target_safety(safety_policy)


使用Jonathan Wakely提供的完美初始化技巧以及临时策略托管,我们可以通过以下方式解决它:

#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