Neste ponto, essa biblioteca é experimental e é uma curiosidade pura. Nenhuma estabilidade da interface ou qualidade da implementação é garantida. Use por seus próprios riscos.
O Dyno resolve o problema do polimorfismo de tempo de execução melhor do que o baunilha C ++. Ele fornece uma maneira de definir interfaces que podem ser cumpridas de forma não-intraus e fornece uma maneira totalmente personalizável de armazenar objetos polimórficos e despachar para métodos virtuais. Não requer herança, alocação de heap ou deixar o mundo confortável da semântica de valor, e pode fazê -lo enquanto supera a baunilha C ++.
O Dyno é a implementação pura da biblioteca do que também é conhecido como objetos de traços de ferrugem, interfaces, classes do tipo Haskell e conceitos virtuais. Sob o capô, ele usa uma técnica C ++ conhecida como apagamento de tipo, que é a idéia por trás da função std::any , std::function e muitos outros tipos úteis.
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}Como alternativa, se você achar que isso é um pouco de caldeira e poderá ficar usando uma macro, o seguinte é equivalente:
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}Esta é uma biblioteca C ++ 17. Não serão feitos esforços para apoiar compiladores mais antigos (desculpe). Sabe -se que a biblioteca trabalha com os seguintes compiladores:
| Compilador | Versão |
|---|---|
| GCC | > = 7 |
| Clang | > = 4.0 |
| Apple Clang | > = 9.1 |
A biblioteca depende de boost.hana e boost.callabletraits. Os testes de unidade dependem do Libawful e dos benchmarks dependem do benchmark do Google, Boost.Typeerasure e MPark.Variant, mas você não precisa deles para usar a biblioteca. Para o desenvolvimento local, o script dependencies/install.sh pode ser usado para instalar todas as dependências automaticamente.
O Dyno é uma biblioteca somente para cabeçalho, então não há nada para construir por se. Basta adicionar o diretório include/ ao seu caminho de pesquisa de cabeçalho do seu compilador (e verifique se as dependências estão satisfeitas) e pronto. No entanto, existem testes de unidade, exemplos e referências que podem ser construídas:
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks Na programação, a necessidade de manipular objetos com uma interface comum, mas com um tipo dinâmico diferente surge com muita frequência. C ++ resolve isso com herança:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}No entanto, essa abordagem tem várias desvantagens. Isso é
Intrusivo
Para que Square e Circle cumpram a interface Drawable , ambos precisam herdar da classe base Drawable . Isso requer ter a licença para modificar essas classes, o que torna a herança muito inentendida. Por exemplo, como você faria um std::vector<int> cumprir a interface Drawable ? Você simplesmente não pode.
Incompatível com semântica de valor
A herança exige que você passe ponteiros polimórficos ou referências a objetos, em vez dos próprios objetos, o que é muito ruim com o restante do idioma e a biblioteca padrão. Por exemplo, como você copiaria um vetor de Drawable s? Você precisaria fornecer um método clone() virtual, mas agora você acabou de estragar sua interface.
Firmemente acoplado ao armazenamento dinâmico
Devido à falta de semântica de valor, geralmente acabamos alocando esses objetos polimórficos na pilha. Isso é terrivelmente ineficiente e semanticamente errado, já que é provável que não precisemos da duração dinâmica de armazenamento, e um objeto com duração automática de armazenamento (por exemplo, na pilha) teria sido suficiente.
Impede a inflinação
95% do tempo, acabamos chamando um método virtual através de um ponteiro ou referência polimórfica. Isso requer três indiretas: uma para carregar o ponteiro para o vtable dentro do objeto, um para carregar a entrada direita no vtable e outro para a chamada indireta para o ponteiro da função. Todo esse salto torna difícil para o compilador tomar boas decisões em linha. No entanto, acontece que todas essas indiretas, exceto a chamada indireta, podem ser evitadas.
Infelizmente, essa é a escolha que o C ++ fez para nós, e essas são as regras às quais estamos vinculados quando precisamos de polimorfismo dinâmico. Ou é realmente?
O Dyno resolve o problema do polimorfismo de tempo de execução em C ++ sem nenhuma das desvantagens listadas acima e muito mais guloseimas. Isso é:
Não intrusivo
Uma interface pode ser cumprida por um tipo sem exigir qualquer modificação para esse tipo. Caramba, um tipo pode até cumprir a mesma interface de maneiras diferentes! Com o Dyno , você pode beijar hierarquias ridículas de classe.
100% com base na semântica de valor
Objetos polimórficos podem ser aprovados como está, com sua semântica de valor natural. Você precisa copiar seus objetos polimórficos? Claro, apenas certifique -se de que eles tenham um construtor de cópias. Você quer ter certeza de que eles não são copiados? Claro, marque -o como excluído. Com os métodos de clone bobo clone() e a proliferação de ponteiros nas APIs são coisas do passado.
Não junto a nenhuma estratégia de armazenamento específica
A maneira como um objeto polimórfico é armazenado é realmente um detalhe de implementação e não deve interferir na maneira como você usa esse objeto. Dyno oferece controle completo sobre a maneira como seus objetos são armazenados. Você tem muitos pequenos objetos polimórficos? Claro, vamos armazená -los em um buffer local e evitar qualquer alocação. Ou talvez faça sentido para você armazenar as coisas na pilha? Claro, vá em frente.
Mecanismo de despacho flexível para alcançar o melhor desempenho possível
Armazenar um ponteiro para um VTable é apenas uma das muitas estratégias de implementação diferentes para realizar o despacho dinâmico. O Dyno oferece controle completo sobre como o despacho dinâmico acontece e pode de fato vencer os VTables em alguns casos. Se você tiver uma função chamada em um loop quente, pode, por exemplo, armazená -la diretamente no objeto e pular a indiretiva vtable. Você também pode usar o conhecimento específico do aplicativo que o compilador nunca poderia otimizar algumas chamadas dinâmicas-desvirtualização no nível da biblioteca.
Primeiro, você começa definindo uma interface genérica e dando um nome. O Dyno fornece uma linguagem específica de domínio simples para fazer isso. Por exemplo, vamos definir uma interface Drawable que descreva tipos que podem ser desenhados:
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { }; Isso define Drawable como representando uma interface para qualquer coisa que tenha um método chamado draw , levando uma referência a um std::ostream . O Dyno chama essas interfaces conceitos dinâmicos , pois descrevem conjuntos de requisitos a serem atendidos por um tipo (como conceitos C ++). No entanto, diferentemente dos conceitos de C ++, esses conceitos dinâmicos são usados para gerar interfaces de tempo de execução, daí o nome dinâmico . A definição acima é basicamente equivalente ao seguinte:
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};Depois que a interface é definida, a próxima etapa é realmente criar um tipo que satisfaz essa interface. Com a herança, você escreveria algo assim:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};Com o Dyno , o polimorfismo não é intrusivo e, em vez disso, é fornecido através do que é chamado de mapa conceitual (após os mapas conceituais C ++ 0x):
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);Este construto é a especialização de um modelo de variável C ++ 14 chamado
concept_mapdefinido no espaço de nomedyno::. Em seguida, inicializamos essa especialização comdyno::make_concept_map(...).
O primeiro parâmetro do lambda é o implícito *this parâmetro está implícito quando declaramos draw como um método acima. Também é possível apagar funções não-membros (consulte a seção relevante).
Este mapa conceitual define como o Square do tipo satisfaz o conceito Drawable . Em certo sentido, ele mapeia o Square do tipo para sua implementação do conceito, que motiva a denominação. Quando um tipo satisfaz os requisitos de um conceito, dizemos que o tipo modela (ou é um modelo de) esse conceito. Agora que o Square é um modelo do conceito Drawable , gostaríamos de usar um Square polimorficamente como um Drawable . Com a herança tradicional, usaríamos um ponteiro para uma classe base como esta:
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{}); Com o Dyno , o polimorfismo e a semântica de valor são compatíveis, e a maneira como os tipos polimórficos são transmitidos pode ser altamente personalizada. Para fazer isso, precisaremos definir um tipo que possa conter qualquer coisa que seja Drawable . É esse tipo, em vez de um Drawable* , que passaremos de e para funções polimórficas. Para ajudar a definir esse invólucro, o Dyno fornece o contêiner dyno::poly , que pode conter um objeto arbitrário que satisfaz um determinado conceito. Como você verá, dyno::poly tem um papel duplo: ele armazena o objeto polimórfico e cuida do despacho dinâmico dos métodos. Tudo o que você precisa fazer é escrever uma embalagem fina sobre dyno::poly para dar exatamente a interface desejada:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};Nota: Você pode usar tecnicamente
dyno::polydiretamente em suas interfaces. No entanto, é muito mais conveniente usar um invólucro com métodos reais do quedyno::polye, portanto, escrever um invólucro é recomendado.
Vamos quebrar isso. Primeiro, definimos um membro poly_ que é um recipiente polimórfico para qualquer coisa que modele o conceito Drawable :
dyno::poly<Drawable> poly_; Em seguida, definimos um construtor que permite construir este contêiner a partir de um tipo arbitrário T :
template < typename T>
drawable (T x) : poly_{x} { } A suposição não dita aqui é que T realmente modela o conceito Drawable . De T Drawable quando você T um dyno::poly Se não houver mapa conceitual, a biblioteca informará que estamos tentando criar um dyno::poly a partir de um tipo que não o suporta, e seu programa não será compilado.
Finalmente, a parte mais estranha e mais importante da definição acima é a do método draw :
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); } O que acontece aqui é que, quando .draw é chamado em nosso objeto drawable , na verdade executaremos um despacho dinâmico para a implementação da função "draw" para o objeto atualmente armazenado no dyno::poly e chama assim. Agora, para criar uma função que aceite qualquer coisa que seja Drawable , não precisa mais se preocupar com os ponteiros e a propriedade da sua interface:
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});A propósito, se você está pensando que tudo isso é estúpido e deveria estar usando um modelo, você está certo. No entanto, considere o seguinte, onde você realmente precisa de polimorfismo de tempo de execução :
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable()); Estritamente falando, você não precisa envolver dyno::poly , mas isso coloca uma boa barreira entre o Dyno e o restante do seu código, que nunca precisa se preocupar com a implementação de sua camada polimórfica. Além disso, ignoramos amplamente como dyno::poly foi implementado na definição acima. No entanto, dyno::poly é um contêiner muito poderoso baseado em políticas para objetos polimórficos que podem ser personalizados para as necessidades de desempenho. Criar um invólucro drawable facilita a ajuste a estratégia de implementação usada pelo dyno::poly para desempenho sem impactar o restante do seu código.
O primeiro aspecto que pode ser personalizado em um dyno::poly é a maneira como o objeto é armazenado dentro do contêiner. Por padrão, simplesmente armazenamos um ponteiro para o objeto real, como se faria com o polimorfismo baseado em herança. No entanto, essa geralmente não é a implementação mais eficiente, e é por isso que dyno::poly permite personalizá -lo. Para fazer isso, basta passar uma política de armazenamento para dyno::poly . Por exemplo, vamos definir nosso invólucro drawable , para que ele tente armazenar objetos até 16 bytes em um buffer local, mas depois volta à pilha se o objeto for maior:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
}; Observe que nada, exceto a política, mudou em nossa definição. Esse é um princípio muito importante do dinamômetro ; Essas políticas são detalhes da implementação e não devem mudar a maneira como você escreve seu código. Com a definição acima, agora você pode criar s drawable como fez antes, e nenhuma alocação acontecerá quando o objeto que você estiver criando o drawable a partir de ajustes em 16 bytes. Quando não se encaixa, no entanto, dyno::poly alocará um buffer grande o suficiente na pilha.
Digamos que você nunca queira fazer uma alocação. Não tem problema, basta alterar a política para dyno::local_storage<16> . Se você tentar construir um drawable a partir de um objeto que seja muito grande para caber no armazenamento local, seu programa não será compilado. Não estamos apenas economizando uma alocação, mas também estamos salvando um indireção de ponteiro toda vez que acessamos o objeto polimórfico se compararmos com a abordagem tradicional baseada em herança. Ao ajustar esses detalhes (importantes) de implementação para você, você pode tornar seu programa muito mais eficiente do que com a herança clássica.
Outras políticas de armazenamento também são fornecidas, como dyno::remote_storage e dyno::non_owning_storage . dyno::remote_storage é o padrão, que sempre armazena um ponteiro em um objeto alocado por heap. dyno::non_owning_storage armazena um ponteiro para um objeto que já existe, sem se preocupar com a vida útil desse objeto. Ele permite a implementação de visualizações polimórficas não proprietárias sobre objetos, o que é muito útil.
As políticas de armazenamento personalizadas também podem ser criadas com bastante facilidade. Veja <dyno/storage.hpp> para obter detalhes.
Quando introduzimos dyno::poly , mencionamos que ele tinha dois papéis; O primeiro é armazenar o objeto polimórfico, e o segundo é executar a expedição dinâmica. Assim como o armazenamento pode ser personalizado, a maneira como a despacho dinâmica é executada também pode ser personalizada usando políticas. Por exemplo, vamos definir nosso invólucro drawable para que, em vez de armazenar um ponteiro para o vtable, ele armazena o vtable no próprio objeto drawable . Dessa forma, evitaremos um indireção cada vez que acessamos uma função virtual:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
}; Observe que nada além da política vtable precisa mudar na definição do nosso tipo drawable . Além disso, se quiséssemos, poderíamos alterar a política de armazenamento independentemente da política vtable. Com o exposto acima, apesar de estarmos economizando todas as indiretas, estamos pagando por ele aumentando nosso objeto drawable (já que ele precisa manter o VTable localmente). Isso pode ser proibitivo se tivéssemos muitas funções no VTable. Em vez disso, faria mais sentido armazenar a maior parte da vtable remotamente, mas apenas embrulhava as poucas funções que chamamos muito. O Dyno facilita muito fazê -lo usando seletores , que podem ser usados para personalizar quais funções uma política se aplica a:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
}; Dada essa definição, o vtable é realmente dividido em dois. A primeira parte é local para o objeto drawable e contém apenas o método draw . A segunda parte é um ponteiro para um vtable em armazenamento estático que contém os métodos restantes (o destruidor, por exemplo).
O Dyno fornece duas políticas vtable, dyno::local<> e dyno::remote<> . Ambas as políticas devem ser personalizadas usando um seletor . Os seletores suportados pela biblioteca são dyno::only<functions...> , dyno::except<...> e dyno::everything_else (que também pode ser escrito dyno::everything ).
Ao definir um conceito, geralmente é o caso de fornecer uma definição padrão para pelo menos algumas funções associadas ao conceito. Por exemplo, por padrão, provavelmente faria sentido usar uma função de membro chamada draw (se houver) para implementar o método abstrato "draw" do conceito Drawable . Para isso, pode -se usar dyno::default_concept_map :
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
); Agora, sempre que tentamos ver como algum tipo T atende ao conceito Drawable , voltaremos ao mapa conceitual padrão se nenhum mapa conceitual foi definido. Por exemplo, podemos criar um novo Circle de tipos:
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle" Circle é automaticamente um modelo de Drawable , embora não tenhamos definido explicitamente um mapa conceitual para Circle . Por outro lado, se definíssemos um mapa conceitual, ele teria precedência sobre o padrão:
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle" Às vezes, é útil definir um mapa conceitual para uma família completa de tipos de uma só vez. Por exemplo, podemos querer fazer std::vector<T> um modelo de Drawable , mas somente quando T puder ser impresso em um fluxo. Isso é facilmente alcançado usando isso (não assim) truque secreto:
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "Observe como não precisamos modificar
std::vector. Como poderíamos fazer isso com o polimorfismo clássico? Resposta: Não pode fazer.
O Dyno permite apagar funções e funções que não são de membros que são despachadas em um argumento arbitrário (mas apenas um argumento) também. Para fazer isso, basta definir o conceito usando a dyno::function em vez de dyno::method e usar o espaço reservado dyno::T para denotar o argumento que está sendo apagado:
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { }; O dyno::T const& Parâmetro usado acima representa o tipo de objeto em que a função está sendo chamada. No entanto, não precisa ser o primeiro parâmetro:
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };O cumprimento do conceito não muda se o conceito usa um método ou uma função, mas verifique se os parâmetros da sua implementação da sua função correspondem à da função declarada no conceito:
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
); Finalmente, ao chamar uma function em um dyno::poly O parâmetro que foi declarado com um espaço reservado dyno::T no conceito deve ser aprovado no próprio dyno::poly :
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};