Сохранение неявности конструкции в классе на основе политики

Рассмотрим класс 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. Сделать публичный деструктор политики — и рискуете UB при приведении Ptr * к policy * и удалении его? Это маловероятно в приведенном примере, но возможно в реальных приложениях.
  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;
}

Однако ошибки не очень читабельны, и мы вводим ненужные требования к политикам, поэтому ответ на первый вопрос предпочтительнее.

6

Решение

Смотрите подход «идеальной инициализации» N4064 за std::pair а также std::tuple который включает в себя тестирование std::is_constructible<T, U>::value а также std::is_convertible<U, T>::value

Если оба являются истинными, существует неявное преобразование, если только первое является истинным, преобразование является явным.

Решение состоит в том, чтобы определить две перегрузки для конструктора, один неявный и один explicitи используйте SFINAE, чтобы была возможна только одна перегрузка.

4

Другие решения

Ну, это заняло у меня некоторое время, чтобы понять, но если проблема заключается в том, что мы не можем создавать объекты типа 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 );
}

Живая демонстрация

Хитрость заключается в том, чтобы направить конструкторов target_policy в target_hostтак что это может быть построено из аргументов target_policy Можно. поскольку Ptr происходит от safety_policy это может быть неявно преобразовано в (const) safety_policy&(&), Это означает, что тестирование конверсии Ptr -> target_host эквивалентно испытательной конструкции target_host::target_safety(safety_policy),


Используя идеальный прием инициализации, предоставленный Джонатаном Уэйкли в сочетании с временным размещением политик, мы можем решить его следующим образом:

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

Живая демо

0

По вопросам рекламы [email protected]