Padrão de modelo curiosamente recorrente

O padrão de modelo curiosamente recorrente ( CRTP ) é um idioma, originalmente em C++ , no qual uma classe Xderiva de uma instanciação de modeloX de classe usando a si mesma como argumento de modelo. [1] Mais geralmente, é conhecido como polimorfismo ligado a F e é uma forma de quantificação limitada a F.

História

A técnica foi formalizada em 1989 como “ quantificação limitada por F ”. [2] O nome "CRTP" foi cunhado independentemente por Jim Coplien em 1995, [3] que o observou em alguns dos primeiros códigos de modelo C++ , bem como em exemplos de código que Timothy Budd criou em sua linguagem multiparadigma Leda. [4] Às vezes é chamado de "Herança de cabeça para baixo" [5] [6] devido à maneira como permite que as hierarquias de classes sejam estendidas, substituindo diferentes classes base.

A implementação do CRTP pela Microsoft na Active Template Library (ATL) foi descoberta de forma independente, também em 1995, por Jan Falkin, que acidentalmente derivou uma classe base de uma classe derivada. Christian Beaumont viu pela primeira vez o código de Jan e inicialmente pensou que ele não poderia compilar no compilador da Microsoft disponível na época. Após a revelação de que realmente funcionou, Christian baseou todo o design do ATL e da Windows Template Library (WTL) nesse erro. [ carece de fontes ]

Forma geral

// O Curiously Recurring Template Pattern (CRTP) 
template < class T > class Base { // métodos dentro de Base podem usar template para acessar membros de Derived }; classe Derivada : public Base < Derivada > { // ... };  
 

    

    

    

Alguns casos de uso para esse padrão são o polimorfismo estático e outras técnicas de metaprogramação, como as descritas por Andrei Alexandrescu em Modern C++ Design . [7] Ele também figura com destaque na implementação C++ do paradigma Dados, Contexto e Interação . [8] Além disso, CRTP é usado pela biblioteca padrão C++ para implementar a std::enable_shared_from_thisfuncionalidade. [9]

Polimorfismo estático

Normalmente, o modelo da classe base aproveitará o fato de que os corpos das funções-membro (definições) não são instanciados até muito depois de suas declarações e usará membros da classe derivada dentro de suas próprias funções-membro, por meio do uso de um cast ; por exemplo:

template < class T > struct Base { void interface () { // ... static_cast < T *> ( this ) -> implementação (); //... }   
 

     
    
        
        
        
    

    static void static_func () { // ... T :: static_sub_func (); //... } };  
    
        
        
        
    


struct Derivado : Base < Derivada > { implementação void (); static void static_sub_func (); };   

     
      

No exemplo acima, a função Base<Derived>::interface(), embora declarada antes da existência de the struct Derivedser conhecida pelo compilador (ou seja, antes Derivedde ser declarada), não é realmente instanciada pelo compilador até que seja realmente chamada por algum código posterior que ocorre após a declaração de Derived(não mostrado no exemplo acima), de modo que no momento em que a função "implementação" for instanciada, a declaração de Derived::implementation()seja conhecida.

Esta técnica consegue um efeito semelhante ao uso de funções virtuais , sem os custos (e alguma flexibilidade) do polimorfismo dinâmico . Este uso específico do CRTP foi chamado de “ligação dinâmica simulada” por alguns. [10] Este padrão é usado extensivamente nas bibliotecas Windows ATL e WTL .

Para elaborar o exemplo acima, considere uma classe base sem funções virtuais . Sempre que a classe base chamar outra função membro, ela sempre chamará suas próprias funções da classe base. Quando derivamos uma classe desta classe base, herdamos todas as variáveis-membro e funções-membro que não foram substituídas (sem construtores ou destruidores). Se a classe derivada chamar uma função herdada que então chama outra função-membro, então essa função nunca chamará nenhuma função-membro derivada ou substituída na classe derivada.

No entanto, se as funções-membro da classe base usarem CRTP para todas as chamadas de função-membro, as funções substituídas na classe derivada serão selecionadas em tempo de compilação. Isso emula efetivamente o sistema de chamada de função virtual em tempo de compilação sem os custos de tamanho ou sobrecarga de chamada de função ( estruturas VTBL e pesquisas de método, máquinas VTBL de herança múltipla) com a desvantagem de não ser capaz de fazer essa escolha em tempo de execução.

Contador de objetos

O objetivo principal de um contador de objetos é recuperar estatísticas de criação e destruição de objetos para uma determinada classe. [11] Isso pode ser facilmente resolvido usando CRTP:

template < typename T > struct counter { static inline int object_created = 0 ; inline estático int objetos_alive = 0 ;  
 

         
         

    contador () 
{ ++ objetos_criados ; ++ objetos_vivos ; } contador ( const contador & ) { ++ objetos_criados ; ++ objetos_vivos ; } protected : ~ counter () //objetos nunca devem ser removidos através de ponteiros deste tipo { --objects_alive ; } };    
        
        
    
    
     
    
        
        
    

     
    
        
    


classe X : contador < X > { // ... };   

    


classe Y : contador < Y > { // ... };   

    

Cada vez que um objeto da classe Xé criado, o construtor de counter<X>é chamado, incrementando a contagem de criados e ativos. Cada vez que um objeto da classe Xé destruído, a contagem de vivos é decrementada. É importante observar que counter<X>e counter<Y>são duas classes separadas e é por isso que manterão contagens separadas de Xs e Ys. Neste exemplo de CRTP, esta distinção de classes é o único uso do parâmetro de modelo ( Tin counter<T>) e a razão pela qual não podemos usar uma classe base simples sem modelo.

Encadeamento polimórfico

O encadeamento de métodos , também conhecido como idioma de parâmetro nomeado, é uma sintaxe comum para invocar múltiplas chamadas de métodos em linguagens de programação orientadas a objetos. Cada método retorna um objeto, permitindo que as chamadas sejam encadeadas em uma única instrução sem a necessidade de variáveis ​​para armazenar os resultados intermediários.

Quando o padrão de objeto de parâmetro nomeado é aplicado a uma hierarquia de objetos, as coisas podem dar errado. Suponha que tenhamos essa classe base:

class Impressora { public : Impressora ( ostream & pstream ) : m_stream ( pstream ) {} template < typename T > Impressora & print ( T && t ) { m_stream << t ; retorne * isto ; } template < typename T > Impressora & println ( T && t ) { m_stream << t << endl ; retorne * isto ; } privado : ostream & m_stream ; }; 


        
 
      
             
 
      
               

     

As impressões podem ser facilmente encadeadas:

Impressora ( meuStream ). println ( "olá" ). imprimir ( 500 );

No entanto, se definirmos a seguinte classe derivada:

class CoutPrinter : impressora pública { public : CoutPrinter () : impressora ( cout ) {}    


       

    CoutPrinter & SetConsoleColor ( Color c ) { // ... return * this ; } };  
    
        
         
    

“perdemos” a classe concreta assim que invocamos uma função da base:

// v----- temos uma 'Impressora' aqui, não uma 'CoutPrinter' 
CoutPrinter (). imprimir ( "Olá" ). SetConsoleColor ( Cor . vermelho ). println ( "Impressora!" ); //erro de compilação 

Isso acontece porque 'print' é uma função da base – 'Printer' – e então retorna uma instância de 'Printer'.

O CRTP pode ser usado para evitar tal problema e implementar o "Encadeamento Polimórfico": [12]


// Modelo de classe base < typename ConcretePrinter > class Printer { public : Printer ( ostream & pstream ) : m_stream ( pstream ) {} template < typename T > ConcretePrinter & print ( T && t ) { m_stream << t ; return static_cast < ConcretoPrinter &> ( * this ); } template < typename T > ConcretePrinter & println ( T && t ) { m_stream << t << endl ; return static_cast < ConcretoPrinter &> ( * this ); } privado : ostream & m_stream ; }; // Classe derivada class CoutPrinter : public Printer < CoutPrinter > { public : CoutPrinter ( ) : Printer ( cout ) {} CoutPrinter & SetConsoleColor ( Color c ) { // ... return * this ; } }; // uso CoutPrinter (). imprimir ( "Olá" ). SetConsoleColor ( Cor . vermelho ). println ( "Impressora!" );  
 


        
 
      
      
    
          
         
    
 
      
      
    
            
         
    

     

 

    


       
 
      
    
        
         
    

 


Construção de cópia polimórfica

Ao usar o polimorfismo, às vezes é necessário criar cópias de objetos pelo ponteiro da classe base. Uma linguagem comumente usada para isso é adicionar uma função de clone virtual definida em cada classe derivada. O CRTP pode ser usado para evitar a duplicação dessa função ou de outras funções semelhantes em cada classe derivada.

// A classe base possui uma função virtual pura para clonar 
class AbstractShape { public : virtual ~ AbstractShape () = default ; virtual std :: unique_ptr < AbstractShape > clone () const = 0 ; };  

        
         


// Esta classe CRTP implementa clone() para 
modelo derivado < typename Derived > class Shape : public AbstractShape { public : std :: unique_ptr < AbstractShape > clone () const override { return std :: make_unique < Derived > ( static_cast < Derived const &> ( * isto )); }  
     

        
          
    

protected : 
// Deixamos claro que a classe Shape precisa ser herdada Shape () = default ; Forma ( const Forma & ) = padrão ; Forma ( Forma && ) = padrão ; };   
     
      
     


// Toda classe derivada herda da classe CRTP em vez da classe abstrata

class Quadrado : forma pública <Quadrado> { } ;    

classe Círculo : forma pública < Círculo > {};    

Isto permite obter cópias de quadrados, círculos ou quaisquer outras formas por meio de arquivos shapePtr->clone().

Armadilhas

Um problema com o polimorfismo estático é que sem usar uma classe base geral como AbstractShapeno exemplo acima, as classes derivadas não podem ser armazenadas de forma homogênea – isto é, colocando diferentes tipos derivados da mesma classe base no mesmo contêiner. Por exemplo, um contêiner definido como std::vector<Shape*>não funciona porque Shapenão é uma classe, mas um modelo que precisa de especialização. Um contêiner definido como std::vector<Shape<Circle>*>só pode armazenar Circles, não Squares. Isso ocorre porque cada uma das classes derivadas da classe base CRTP Shapeé um tipo exclusivo. Uma solução comum para esse problema é herdar de uma classe base compartilhada com um destruidor virtual, como no AbstractShapeexemplo acima, permitindo a criação de um arquivo std::vector<AbstractShape*>.

Deduzindo isso

O uso do CRTP pode ser simplificado usando o recurso C++23 , deduzindo isso. [13] [14] Para que a função signature_dishchame uma função membro derivada cook_signature_dish, ChefBaseela precisa ser um tipo de modelo e CafeChefherdar de ChefBase, passando seu tipo como parâmetro do modelo.

template < typename T > struct ChefBase { void subscription_dish () { static_cast < T *> ( this ) -> cook_signature_dish (); } };  
 

     
    
        
    


struct CafeChef : ChefBase <CafeChef> { void cook_signature_dish () { } } ;   

      

Se o parâmetro de objeto explícito for usado, ChefBasenão precisa ser modelado e CafeChefpode derivar de ChefBaseforma simples. Como o selfparâmetro é deduzido automaticamente como o tipo derivado correto, nenhuma conversão é necessária.

struct ChefBase { template < typename Self > void subscription_dish ( this Self && self ) { self . cook_signature_dish (); } }; 

      
       
    
        
    


estrutura CafeChef : ChefBase { void cook_signature_dish () {} };   

      

Veja também

Referências

  1. ^ Abraãos, David ; Gurtovoy, Aleksey (janeiro de 2005). Metaprogramação de modelo C++: conceitos, ferramentas e técnicas de Boost e Beyond . Addison-Wesley. ISBN 0-321-22725-5.
  2. ^ William Cozinheiro; e outros. (1989). "Polimorfismo limitado por F para programação orientada a objetos" (PDF) .
  3. ^ Coplien, James O. (fevereiro de 1995). "Padrões de modelos curiosamente recorrentes" (PDF) . Relatório C++ : 24–27.
  4. ^ Budd, Timothy (1994). Programação multiparadigma em Leda. Addison-Wesley. ISBN 0-201-82080-3.
  5. ^ "Café Apóstata: ATL e Herança Invertida" . 15 de março de 2006. Arquivado do original em 15 de março de 2006 . Recuperado em 9 de outubro de 2016 .{{cite web}}: CS1 maint: bot: status do URL original desconhecido ( link )
  6. ^ "ATL e herança invertida" . 4 de junho de 2003. Arquivado do original em 4 de junho de 2003 . Recuperado em 9 de outubro de 2016 .{{cite web}}: CS1 maint: bot: status do URL original desconhecido ( link )
  7. ^ Alexandrescu, Andrei (2001). Design C++ moderno: programação genérica e padrões de design aplicados . Addison-Wesley. ISBN 0-201-70431-5.
  8. ^ Coplien, James ; Bjornvig, Gertrud (2010). Arquitetura Lean: para desenvolvimento ágil de software . Wiley. ISBN 978-0-470-68420-7.
  9. ^ "std::enable_shared_from_this" . Recuperado em 22 de novembro de 2022 .
  10. ^ "Encadernação Dinâmica Simulada" . 7 de maio de 2003. Arquivado do original em 9 de fevereiro de 2012 . Recuperado em 13 de janeiro de 2012 .
  11. ^ Meyers, Scott (abril de 1998). "Contando objetos em C++". Diário de usuários C/C++ .
  12. ^ Arena, Marco (29 de abril de 2012). "Use CRTP para encadeamento polimórfico" . Recuperado em 15 de março de 2017 .
  13. ^ Gasper Ažman; Marca Sy; Ben Deane; Barry Revzin (12 de julho de 2021). "Deduzindo isso".
  14. ^ "Parâmetro de objeto explícito" . Recuperado em 27 de dezembro de 2023 .
Obtido em "https://en.wikipedia.org/w/index.php?title=Curiosamente_recurring_template_pattern&oldid=1198326047"