Lamentando a ausência em C ++ de uma estática completa se ou ...

... como preencher uma classe de modelo com conteúdos diferentes, dependendo dos valores dos parâmetros do modelo?


Era uma vez, por algum tempo, que a linguagem D começou a ser feita como "o C ++ correto", levando em conta a experiência adquirida em C ++. Com o tempo, D se tornou uma linguagem menos complexa e mais expressiva que o C ++. E o C ++ já começou a espionar D. Por exemplo, apareceu no C ++ 17 if constexpr , na minha opinião, é um empréstimo direto de D, cujo protótipo era D-shny estático se .


Infelizmente, if constexpr no C ++ não tem o mesmo poder que o static if no D. Há razões para isso , mas ainda existem casos em que você só pode se arrepender de que if constexpr no C ++ não permite controlar o conteúdo do C + + classe. Eu gostaria de falar sobre um desses casos.


Falaremos sobre como criar uma classe de modelo, cujo conteúdo (isto é, a composição dos métodos e a lógica de alguns dos métodos) mudaria dependendo de quais parâmetros foram passados ​​para essa classe de modelo. Um exemplo é retirado da vida real, da experiência de desenvolver uma nova versão do SObjectizer .


A tarefa a ser resolvida


É necessário criar uma versão inteligente do "ponteiro inteligente" para armazenar objetos de mensagem. Para que você possa escrever algo como:


 message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg); 

O truque dessa classe message_holder_t é que existem três fatores importantes a serem considerados.


Qual é o tipo de mensagem herdada?


Os tipos de mensagens que parametrizam message_holder_t são divididos em dois grupos. O primeiro grupo são mensagens herdadas do tipo base especial message_t . Por exemplo:


 struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} }; 

Nesse caso, o message_holder_t dentro de si deve conter apenas um ponteiro para um objeto desse tipo. O mesmo ponteiro deve ser retornado nos métodos getter. Ou seja, para o caso do herdeiro de message_t deve haver algo como:


 template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } }; 

O segundo grupo são mensagens de tipos arbitrários de usuários que não são herdadas de message_t . Por exemplo:


 struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} }; 

Instâncias desses tipos no SObjectizer não são enviadas por si mesmas, mas são incluídas em um wrapper especial, user_type_message_t<M> , que já é herdado de message_t . Portanto, para esses tipos, message_holder_t deve conter um ponteiro para user_type_message_t<M> dentro dele, e os métodos getter devem retornar um ponteiro para M:


 template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } }; 

Imunidade ou mutabilidade das mensagens


O segundo fator é a divisão das mensagens em imutável e mutável. Se a mensagem é imutável (e, por padrão, é imutável), os métodos getter devem retornar um ponteiro constante para a mensagem. E se mutável, os getters devem retornar um ponteiro não constante. I.e. deve ser algo como:


 message_holder_t<so5_message> msg1{...}; //  . const int a = msg1->a_; // OK. msg1->a_ = 0; //     ! message_holder_t<mutable_msg<user_message>> msg2{...}; //  . const int a = msg2->a_; // OK. msg2->a_ = 0; // OK. 

shared_ptr vs unique_ptr


O terceiro fator é a lógica do comportamento de message_holder_t como um ponteiro inteligente. Uma vez que ele deve se comportar como std::shared_ptr , ou seja, Você pode ter vários message_holders referindo-se à mesma instância de mensagem. E uma vez que ele deve se comportar como std::unique_ptr , ou seja, apenas uma instância message_holder pode se referir a uma instância de mensagem.


Por padrão, o comportamento do message_holder_t deve depender da mutabilidade / imutabilidade da mensagem. I.e. com mensagens imutáveis, message_holder_t deve se comportar como std::shared_ptr e com std::unique_ptr mutáveis ​​como std::unique_ptr :


 message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg; // OK. message_holder_t<mutable_msg<user_message>> msg3{...}; message_holder_t<mutable_msg<user_message>> msg4 = msg3; // !  ! message_holder_t<mutable_msg<user_message>> msg5 = std::move(msg3); // OK. 

Mas a vida é uma coisa complicada, então você também deve poder definir manualmente o comportamento do message_holder_t . Para que você possa criar message_holder para uma mensagem imutável que se comporte como unique_ptr. E para que você possa criar message_holder para uma mensagem mutável que se comporte como shared_ptr:


 using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1; // !  ! unique_so5_message msg3 = std::move(msg); // OK,   msg3. using shared_user_messsage = so_5::message_holder_t< so_5::mutable_msg<user_message>, so_5::message_ownership_t::shared>; shared_user_message msg4{...}; shared_user_message msg5 = msg4; // OK. 

Assim, quando message_holder_t funciona como shared_ptr, ele deve ter o conjunto usual de construtores e operadores de atribuição: copiar e mover. Além disso, deve haver um método constante make_reference , que retorna uma cópia do ponteiro armazenado em message_holder_t .


Mas quando message_holder_t funciona como unique_ptr, o construtor e o operador de cópia devem ser proibidos. E o método make_reference deve make_reference o ponteiro do objeto message_holder_t : depois de chamar make_reference message_holder_t original deve permanecer vazio.


Um pouco mais formal


Então, você precisa criar uma classe de modelo:


 template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...}; 

quais:


  • o interior deve ser armazenado intrusive_ptr_t<M> ou intrusive_ptr<user_type_message_t<M>> dependendo de M ser herdado de message_t ;
  • Os métodos getter devem retornar const M* ou M* dependendo da mutabilidade / imutabilidade da mensagem;
  • deve haver um conjunto completo de construtores e operadores de copiar / mover, ou apenas um operador construtor e de mover;
  • O método make_reference() deve retornar uma cópia do intrusive_ptr armazenado ou pegar o valor de intrusive_ptr e deixar o message_holder_t original vazio. No primeiro caso, make_reference() deve ser constante, no segundo método não constante.

Os dois últimos itens da lista são determinados pelo parâmetro Ownership (bem como a mutabilidade da mensagem se a autodetected usada para a propriedade).


Como foi decidido


Nesta seção, consideraremos todos os componentes que compõem a solução final. Bem, a solução resultante em si. Os fragmentos de código limpos de todos os detalhes de distração serão mostrados. Se alguém estiver interessado no código real, você pode vê-lo aqui .


Isenção de responsabilidade


A solução mostrada abaixo não finge ser bonita, ideal ou um modelo. Foi encontrado, implementado, testado e documentado em pouco tempo, sob pressão de prazos. Talvez se houvesse mais tempo e mais se empenhasse na busca de uma solução jovem sensato e conhecedor do desenvolvedor de C ++ moderno, ele se tornaria mais compacto, mais simples e mais compreensível. Mas, como se viu, aconteceu ... "Não atire no pianista", em geral.


Sequência de etapas e magia de modelo pronta


Portanto, precisamos ter uma classe com vários conjuntos de métodos. O conteúdo desses kits deve vir de algum lugar. De onde


Em D, poderíamos usar static if e definir diferentes partes da classe, dependendo das diferentes condições. Em alguns Ruby, podemos misturar métodos em nossa classe usando o método include . Mas estamos em C ++, onde até agora nossas possibilidades são muito limitadas: podemos definir um método / atributo diretamente dentro da classe, ou podemos herdar o método / atributo de alguma classe base.


Não podemos definir métodos / atributos diferentes dentro da classe, dependendo de alguma condição, porque C ++ if constexpr não for D static if . Consequentemente, apenas a herança permanece.


Upd. Como sugerido nos comentários, devo falar com mais cuidado aqui. Como o C ++ possui SFINAE, podemos ativar / desativar a visibilidade de métodos individuais na classe via SFINAE (ou seja, obter um efeito semelhante ao static if ). Mas essa abordagem tem duas falhas sérias, na minha opinião,. Primeiramente, se esses métodos não forem 1-2-3, mas 4-5 ou mais, é tedioso formatar cada um deles usando SFINAE, e isso afeta a legibilidade do código. Em segundo lugar, o SFINAE não nos ajuda a adicionar / remover atributos de classe (campos).

Em C ++, podemos definir várias classes base das quais herdaremos message_holder_t . E a escolha de uma ou outra classe base já será feita dependendo dos valores dos parâmetros do modelo, usando std :: condicional .


Mas o truque é que precisamos não apenas de um conjunto de classes base, mas de uma pequena cadeia de herança. No início, haverá uma classe que determinará a funcionalidade geral que será necessária em qualquer caso. A seguir, serão apresentadas as classes base que determinarão a lógica do comportamento do "ponteiro inteligente". E então haverá uma classe que determina os getters necessários. Nesta ordem, consideraremos as classes implementadas.


Nossa tarefa é simplificada pelo fato de o SObjectizer já possuir uma mágica de modelo pronta que determina se uma mensagem é herdada de message_t , bem como meios para verificar a mutabilidade da mensagem . Portanto, na implementação, simplesmente usaremos essa mágica pronta e não nos aprofundaremos nos detalhes de seu trabalho.


Base de armazenamento comum do ponteiro


Vamos começar com um tipo de base comum que armazena o intrusive_ptr correspondente e também fornece um conjunto comum de métodos que qualquer uma das implementações message_holder_t precisa:


 template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } }; 

Esta classe de modelo possui dois parâmetros. O primeiro, Payload, define o tipo que os métodos getter devem usar. Enquanto o segundo, Envelope, define o tipo para intrusive_ptr. No caso em que o tipo de mensagem é herdado de message_t esses dois parâmetros terão o mesmo valor. Mas se a mensagem não for herdada de message_t , o tipo de mensagem será usado como Carga útil e user_type_message_t<Payload> será user_type_message_t<Payload> como Envelope.


Eu acho que basicamente o conteúdo desta aula não levanta questões. Mas duas coisas devem ser anotadas separadamente.


Em primeiro lugar, o ponteiro em si, ou seja, o atributo m_msg é definido na seção protegida para que os herdeiros das classes tenham acesso a ele.


Em segundo lugar, para esta classe, o próprio compilador gera todos os construtores necessários e operadores de copiar / mover. E no nível desta classe, ainda não proibimos nada.


Bases separadas para o comportamento shared_ptr e unique_ptr


Portanto, temos uma classe que armazena um ponteiro para uma mensagem. Agora podemos definir seus herdeiros, que se comportarão como shared_ptr ou unique_ptr.


Vamos começar com o caso do comportamento shared_ptr, porque aqui está o menor código:


 template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } }; 

Nada complicado: herdar de basic_message_holder_impl_t , herdar todos os seus construtores e definir uma implementação simples e não destrutiva de make_reference() .


Para o caso de unique_ptr-behavior, o código é maior, embora não haja nada complicado:


 template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } }; 

Novamente, herdamos de basic_message_holder_impl_t e herdamos os construtores que precisamos dele (este é o construtor padrão e o construtor de inicialização). Mas, ao mesmo tempo, definimos os construtores e os operadores de copiar / mover de acordo com a lógica unique_ptr: proibimos a cópia, implementamos a movimentação.


Também temos um método destrutivo make_reference() .


Isso é tudo, na verdade. Resta apenas realizar a escolha entre essas duas classes base ...


Escolhendo entre o comportamento shared_ptr e unique_ptr


Para escolher entre o comportamento shared_ptr e unique_ptr, você precisa da seguinte metafunção (metafunction porque "funciona" com tipos em tempo de compilação):


 template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; }; 

Essa metafunção aceita os dois parâmetros da lista de parâmetros message_holder_t e, como resultado (ou seja, a definição de um type aninhado), "retorna" o tipo do qual deve ser herdado. I.e. shared_message_holder_impl_t ou unique_message_holder_impl_t .


Dentro da definição de impl_selector você pode ver traços da mágica mencionada acima e sobre os quais não analisamos: message_payload_type<Msg>::payload_type , message_payload_type<Msg>::envelope_type e message_mutability_traits<Msg>::mutability .


E para usar a função impl_selector foi mais fácil, definiremos um nome mais curto para ela:


 template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type; 

Base para getters


Portanto, já temos a oportunidade de selecionar uma base que contém um ponteiro e define o comportamento de um "ponteiro inteligente". Agora precisamos fornecer a essa base os métodos getter. Por que precisamos de uma classe simples:


 template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } }; 

Esta é uma classe de modelo que depende de dois parâmetros, mas seu significado é completamente diferente. O parâmetro Base será o resultado da impl_selector impl_selector mostrada acima. I.e. como o parâmetro Base, a classe base é definida da qual herdar.


É importante observar que, se a herança vier de unique_message_holder_impl_t , para a qual o construtor e o operador de cópia são proibidos, o compilador não poderá gerar o construtor e o operador de cópia para o msg_accessors_t . É disso que precisamos.


O tipo da mensagem, o ponteiro / link ao qual será retornado pelos getters, atuará como o parâmetro Return_Type. O truque é que, para uma mensagem imutável do tipo Msg o parâmetro Return_Type será definido como const Msg . Enquanto que para uma mensagem mutável do tipo Msg parâmetro Return_Type terá o valor Msg . Assim, o método get() retornará const Msg* para mensagens imutáveis ​​e apenas Msg* para mensagens mutáveis.


Usando a função livre get_ptr() resolvemos o problema de trabalhar com mensagens que não são herdadas de message_t :


 template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); } 

I.e. se a mensagem não for herdada de message_t e armazenada como user_type_message_t<Msg> , a segunda sobrecarga será chamada. E se for herdado, então a primeira sobrecarga.


Escolhendo uma base específica para getters


Portanto, o modelo msg_accessors_t requer dois parâmetros. O primeiro é calculado pela impl_selector impl_selector. Mas, para formar um tipo de base específico a partir de msg_accessors_t , precisamos determinar o valor do segundo parâmetro. Mais uma meta-função é destinada a isso:


 template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; }; 

Você só pode prestar atenção ao cálculo do parâmetro Return_Type. Um daqueles poucos casos em que const leste é útil;)


Bem, para aumentar a legibilidade do código a seguir, uma opção mais compacta para trabalhar com ele:


 template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type; 

Sucessor final message_holder_t


Agora você pode ver o que message_holder_t , para a implementação da qual todas essas classes base e metafunções foram necessárias (parte dos métodos para construir uma instância da mensagem armazenada em message_holder são removidos da implementação):


 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } }; 

De fato, tudo o que analisamos acima era necessário para registrar essa "chamada" de duas metafunções:


 details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > 

Porque essa não é a primeira opção, mas, como resultado da simplificação e redução do código, posso dizer que formas compactas de metafunções reduzem bastante a quantidade de código e aumentam sua compreensibilidade (se é geralmente apropriado falar aqui sobre compreensibilidade).


E o que aconteceria se ...


Mas se em C ++ if constexpr fosse tão poderoso quanto static if em D, você poderia escrever algo como:


Versão hipotética com mais avançado se constexpr
 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; }; 

, . C++ :(
( C++ "" ).


, , ++. , , , . , message_holder_t . , , if constexpr .


Conclusão


, C++. , . , , .


, .


, , ++ , . , . , , . , . C++98/03 , C++11 .

Source: https://habr.com/ru/post/pt449122/


All Articles