6

考虑一个基于策略的智能指针类 Ptr,它只有一个策略可以防止在 NULL 状态下(以某种方式)取消引用它。让我们考虑 2 种此类策略:

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

活生生的例子

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

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

至于我认为不是最优的明显解决方案:

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

问题是:

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

或者:

从宿主类的构造函数调用策略构造函数时,如何保留隐式构造的信息?


编辑:

我刚刚意识到第二个问题可以很容易地用一个私有的、隐式标记的构造函数来回答,如下所示:

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

然而,这些错误不是很可读,并且我们对策略引入了不必要的要求,因此对第一个问题的回答更可取。

4

2 回答 2

4

请参阅N4064采用的“完美初始化”方法,std::pair其中std::tuple涉及测试std::is_constructible<T, U>::valuestd::is_convertible<U, T>::value

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

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

于 2014-08-27T11:30:56.310 回答
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 );
  }

现场演示

诀窍是转发 to 的构造函数target_policytarget_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;
}

现场演示

于 2014-08-27T15:59:09.623 回答