Quero dizer, além de seu nome obrigatório (a Biblioteca de modelos padrão) ...
O C ++ inicialmente pretendeu apresentar conceitos de POO em C. Ou seja: você poderia dizer o que uma entidade específica poderia ou não fazer (independentemente de como o faz) com base em sua classe e hierarquia de classes. Algumas composições de habilidades são mais difíceis de descrever dessa maneira devido aos problemas de herança múltipla e ao fato de o C ++ suportar o conceito de interfaces de uma maneira um pouco desajeitada (em comparação com java, etc.), mas existe (e pode ser melhorado).
E então os modelos entraram em jogo, junto com o STL. O STL parecia pegar os conceitos clássicos de POO e liberá-los pelo ralo, usando modelos.
Deve haver uma distinção entre os casos em que os modelos são usados para generalizar tipos nos quais os tipos são mostrados como irrelevantes para a operação do modelo (contêineres, por exemplo). Ter um vector<int>
faz todo o sentido.
No entanto, em muitos outros casos (iteradores e algoritmos), os tipos de modelo devem seguir um "conceito" (Iterador de entrada, Iterador de avanço, etc ...) em que os detalhes reais do conceito são definidos inteiramente pela implementação do modelo função / classe, e não pela classe do tipo usado com o modelo, que é um pouco anti-uso do OOP.
Por exemplo, você pode dizer a função:
void MyFunc(ForwardIterator<...> *I);
Atualização: como não estava claro na pergunta original, o ForwardIterator pode ser modelado para permitir qualquer tipo de ForwardIterator. O contrário é ter o ForwardIterator como um conceito.
espera um Iterador Direto apenas observando sua definição, na qual você precisaria examinar a implementação ou a documentação para:
template <typename Type> void MyFunc(Type *I);
Duas reivindicações que posso fazer em favor do uso de modelos: o código compilado pode ser mais eficiente, compilando o modelo para cada tipo usado, em vez de usar vtables. E o fato de que os modelos podem ser usados com tipos nativos.
No entanto, estou procurando uma razão mais profunda por que abandonar a POO clássica em favor da criação de modelos para o STL? (Supondo que você leu até aqui: P)
vector<int>
evector<char>
para ser usado ao mesmo tempo. Eles podem, com certeza, mas você pode usar quaisquer dois pedaços de código ao mesmo tempo. Isso não tem nada a ver com modelos, C ++ ou STL. Não há nada na instanciaçãovector<int>
que exija que ovector<char>
código seja carregado ou executado.Respostas:
A resposta curta é "porque o C ++ mudou". Sim, no final dos anos 70, o Stroustrup pretendia criar um C atualizado com recursos de OOP, mas isso foi há muito tempo. Quando o idioma foi padronizado em 1998, não era mais um idioma OOP. Era uma linguagem multiparadigma. Certamente tinha algum suporte para o código OOP, mas também tinha uma linguagem de modelo completa, sobreposta, permitiu a metaprogramação em tempo de compilação e as pessoas descobriram a programação genérica. De repente, OOP simplesmente não parecia tão importante. Não quando podemos escrever código mais simples, conciso e eficiente usando técnicas disponíveis através de modelos e programação genérica.
OOP não é o Santo Graal. É uma idéia bonitinha, e foi uma melhoria bastante em relação às linguagens processuais nos anos 70, quando foi inventada. Mas, honestamente, nem tudo é o que parece. Em muitos casos, é desajeitado e detalhado e realmente não promove código ou modularidade reutilizável.
É por isso que a comunidade C ++ hoje está muito mais interessada em programação genérica e por que todos estão finalmente começando a perceber que a programação funcional também é bastante inteligente. OOP por si só não é uma visão bonita.
Tente desenhar um gráfico de dependência de um STL hipotético "OOP-ified". Quantas aulas precisariam saber uma sobre a outra? Haveria muitas dependências. Você seria capaz de incluir apenas o
vector
cabeçalho, sem também ser puxadoiterator
ouiostream
puxado? O STL facilita isso. Um vetor conhece o tipo de iterador que define, e é tudo. Os algoritmos STL não sabem nada . Eles nem precisam incluir um cabeçalho de iterador, mesmo que todos aceitem iteradores como parâmetros. Qual é mais modular então?O STL pode não seguir as regras do OOP conforme o Java o define, mas não alcança os objetivos do OOP? Não consegue reutilização, baixo acoplamento, modularidade e encapsulamento?
E não alcança esses objetivos melhor do que uma versão OOP?
Quanto ao motivo pelo qual o STL foi adotado na linguagem, várias coisas aconteceram que o levaram ao STL.
Primeiro, os modelos foram adicionados ao C ++. Eles foram adicionados pelo mesmo motivo pelo qual os genéricos foram adicionados ao .NET. Parecia uma boa idéia ser capaz de escrever coisas como "recipientes do tipo T" sem jogar fora a segurança do tipo. Obviamente, a implementação em que eles se estabeleceram foi muito mais complexa e poderosa.
Então, as pessoas descobriram que o mecanismo de modelo que eles haviam adicionado era ainda mais poderoso do que o esperado. E alguém começou a experimentar o uso de modelos para escrever uma biblioteca mais genérica. Um inspirado na programação funcional e outro que utilizava todos os novos recursos do C ++.
Ele o apresentou ao comitê de linguagem C ++, que demorou um pouco para se acostumar, porque parecia tão estranho e diferente, mas finalmente percebeu que funcionava melhor do que os equivalentes tradicionais de POO que eles teriam que incluir de outra forma . Então, eles fizeram alguns ajustes e o adotaram na biblioteca padrão.
Não foi uma escolha ideológica, não foi uma escolha política de "queremos ser OOP ou não", mas uma escolha muito pragmática. Eles avaliaram a biblioteca e viram que ela funcionava muito bem.
De qualquer forma, os dois motivos mencionados para favorecer o STL são absolutamente essenciais.
Biblioteca padrão do C ++ tem de ser eficiente. Se for menos eficiente do que, digamos, o código C enrolado à mão equivalente, as pessoas não o usariam. Isso reduziria a produtividade, aumentaria a probabilidade de erros e, no geral, seria apenas uma má idéia.
E o STL precisa trabalhar com tipos primitivos, porque tipos primitivos são tudo o que você tem em C e são a maior parte dos dois idiomas. Se o STL não funcionasse com matrizes nativas, seria inútil .
Sua pergunta tem uma forte suposição de que OOP é "melhor". Estou curioso para saber o porquê. Você pergunta por que eles "abandonaram o POO clássico". Estou me perguntando por que eles deveriam ter ficado com isso. Que vantagens teria?
fonte
std::set
como exemplo. Não herda de uma classe base abstrata. Como isso limita o seu usostd::set
? Existe algo que você não pode fazer com umstd::set
porque ele não herda de uma classe base abstrata?A resposta mais direta ao que você está perguntando / reclamando é a seguinte: A suposição de que C ++ é uma linguagem OOP é uma suposição falsa.
C ++ é uma linguagem de múltiplos paradigmas. Ele pode ser programado usando princípios de POO, pode ser programado proceduralmente, pode ser programado genericamente (modelos) e com o C ++ 11 (anteriormente conhecido como C ++ 0x) algumas coisas podem ser programadas funcionalmente.
Os projetistas do C ++ veem isso como uma vantagem, portanto argumentam que restringir o C ++ a agir como uma linguagem puramente OOP quando a programação genérica resolve melhor o problema e, bem, de maneira mais genérica , seria um passo para trás.
fonte
Meu entendimento é que o Stroustrup originalmente preferia um design de contêiner "estilo OOP" e, de fato, não via outra maneira de fazê-lo. Alexander Stepanov é o responsável pelo STL, e seus objetivos não incluíam "torná-lo orientado a objetos" :
(Ele explica por que a herança e os virtuais - também conhecido como design orientado a objetos "eram fundamentalmente falhos e não deveriam ser usados" no restante da entrevista).
Depois que Stepanov apresentou sua biblioteca para Stroustrup, Stroustrup e outros passaram por esforços hercéticos para inseri-la no padrão ISO C ++ (mesma entrevista):
fonte
A resposta é encontrada nesta entrevista com Stepanov, o autor do STL:
fonte
Por que um design puro de POO para uma Biblioteca de Estrutura e Algoritmos de Dados seria melhor ?! OOP não é a solução para tudo.
IMHO, STL é a biblioteca mais elegante que eu já vi :)
para sua pergunta,
você não precisa de polimorfismo de tempo de execução, é uma vantagem para a STL realmente implementar a Biblioteca usando polimorfismo estático, o que significa eficiência. Tente escrever uma Classificação ou Distância genérica ou qualquer algoritmo que se aplique a TODOS os contêineres! seu Sort in Java chamaria funções dinâmicas através de n-levels a serem executadas!
Você precisa de coisas estúpidas como Boxe e Unboxing para esconder suposições desagradáveis das chamadas linguagens Pure OOP.
O único problema que vejo com o STL e os modelos em geral são as terríveis mensagens de erro. Que será resolvido usando conceitos em C ++ 0X.
Comparar STL a coleções em Java é como comparar Taj Mahal à minha casa :)
fonte
static_assert
talvez.Eu acho que você entende mal o uso pretendido dos conceitos pelos modelos. O Iterator para frente, por exemplo, é um conceito muito bem definido. Para encontrar as expressões que devem ser válidas para que uma classe seja um Iterador Direto e suas semânticas, incluindo a complexidade computacional, consulte o padrão ou http://www.sgi.com/tech/stl/ForwardIterator.html (você precisa seguir os links para Entrada, Saída e Trivial Iterator para ver tudo).
Esse documento é uma interface perfeitamente boa e "os detalhes reais do conceito" são definidos ali mesmo. Eles não são definidos pelas implementações dos Forward Iterators, nem pelos algoritmos que usam os Forward Iterators.
As diferenças em como as interfaces são tratadas entre STL e Java são triplas:
1) STL define expressões válidas usando o objeto, enquanto Java define métodos que devem ser chamados no objeto. Obviamente, uma expressão válida pode ser uma chamada de método (função de membro), mas não precisa ser.
2) As interfaces Java são objetos de tempo de execução, enquanto os conceitos STL não são visíveis no tempo de execução, mesmo com o RTTI.
3) Se você não conseguir validar as expressões válidas necessárias para um conceito STL, receberá um erro de compilação não especificado ao instanciar algum modelo com o tipo. Se você não conseguir implementar um método necessário de uma interface Java, receberá um erro de compilação específico dizendo isso.
Esta terceira parte é se você gosta de uma espécie de "digitação de pato" (em tempo de compilação): as interfaces podem estar implícitas. Em Java, as interfaces são um pouco explícitas: uma classe "é" Iterável se e somente se diz que implementa Iterable. O compilador pode verificar se as assinaturas de seus métodos estão todas presentes e corretas, mas a semântica ainda está implícita (ou seja, está documentada ou não, mas apenas mais código (testes de unidade) pode indicar se a implementação está correta).
No C ++, como no Python, a semântica e a sintaxe estão implícitas, embora no C ++ (e no Python, se você obtiver o pré-processador de digitação forte), obtenha ajuda do compilador. Se um programador exigir declaração explícita de interfaces do tipo Java pela classe de implementação, a abordagem padrão é usar traços de tipo (e a herança múltipla pode impedir que isso seja muito detalhado). O que está faltando, em comparação com Java, é um modelo único que eu posso instanciar com meu tipo e que será compilado se e somente se todas as expressões necessárias forem válidas para meu tipo. Isso me diria se eu implementei todos os bits necessários "antes de usá-lo". Essa é uma conveniência, mas não é o cerne da OOP (e ainda não testa semântica,
O STL pode ou não ser suficientemente OO para o seu gosto, mas certamente separa a interface de maneira limpa da implementação. Não possui a capacidade do Java de refletir sobre interfaces e relata violações dos requisitos de interface de maneira diferente.
Pessoalmente, acho que os tipos implícitos são uma força, quando usados adequadamente. O algoritmo diz o que faz com seus parâmetros de modelo e o implementador garante que essas coisas funcionem: é exatamente o denominador comum do que "interfaces" devem fazer. Além disso, com o STL, é improvável que você esteja usando, digamos, com
std::copy
base em encontrar sua declaração de encaminhamento em um arquivo de cabeçalho. Os programadores devem estar trabalhando no que uma função leva com base em sua documentação, não apenas na assinatura da função. Isso é verdade em C ++, Python ou Java. Existem limitações sobre o que pode ser alcançado com a digitação em qualquer idioma, e tentar usar a digitação para fazer algo que não faz (verifique a semântica) seria um erro.Dito isto, os algoritmos STL geralmente nomeiam seus parâmetros de modelo de uma maneira que deixa claro qual conceito é necessário. No entanto, isso é para fornecer informações extras úteis na primeira linha da documentação, para não tornar as declarações avançadas mais informativas. Há mais coisas que você precisa saber do que o que pode ser encapsulado nos tipos de parâmetros; portanto, você deve ler os documentos. (Por exemplo, em algoritmos que utilizam um intervalo de entrada e um iterador de saída, é provável que o iterador de saída precise de "espaço" suficiente para um determinado número de saídas com base no tamanho da faixa de entrada e talvez nos valores contidos nela. Tente digitar isso com força. )
Aqui está Bjarne em interfaces explicitamente declaradas: http://www.artima.com/cppsource/cpp0xP.html
Analisando o contrário, com a digitação de pato, você pode implementar uma interface sem saber que ela existe. Ou alguém pode escrever uma interface deliberadamente, para que sua classe a implemente, após consultar seus documentos para ver se eles não pedem nada que você ainda não faz. Isso é flexível.
fonte
std
biblioteca que não corresponde a um conceito geralmente é "mal formado, não é necessário diagnóstico"."OOP para mim significa apenas mensagens, retenção e proteção local e ocultação de processos estatais, e ligação tardia extrema de todas as coisas. Isso pode ser feito no Smalltalk e no LISP. Existem outros sistemas nos quais isso é possível, mas possivelmente Eu não estou ciente deles. " - Alan Kay, criador do Smalltalk.
C ++, Java e a maioria das outras linguagens estão bem longe do POO clássico. Dito isto, argumentar por ideologias não é terrivelmente produtivo. C ++ não é puro em nenhum sentido, por isso implementa funcionalidades que parecem fazer sentido pragmático no momento.
fonte
O STL começou com a intenção de fornecer uma grande biblioteca cobrindo o algoritmo mais comumente usado - com o objetivo de comportamento e desempenho consistentes . O modelo veio como um fator-chave para viabilizar essa implementação e destino.
Apenas para fornecer outra referência:
Al Stevens entrevista Alex Stepanov, em março de 1995, do DDJ:
Stepanov explicou sua experiência de trabalho e sua escolha em relação a uma grande biblioteca de algoritmos, que acabou evoluindo para o STL.
fonte
O problema básico com
é como você obtém com segurança o tipo de coisa que o iterador retorna? Com modelos, isso é feito para você no momento da compilação.
fonte
Por um momento, vamos pensar na biblioteca padrão como basicamente um banco de dados de coleções e algoritmos.
Se você estudou o histórico dos bancos de dados, sem dúvida sabe que, no início, os bancos de dados eram principalmente "hierárquicos". Os bancos de dados hierárquicos correspondiam muito à OOP clássica - especificamente, a variedade de herança única, como a usada pelo Smalltalk.
Com o tempo, ficou claro que bancos de dados hierárquicos poderiam ser usados para modelar quase tudo, mas em alguns casos o modelo de herança única era bastante limitante. Se você tivesse uma porta de madeira, seria útil vê-la como uma porta ou como uma peça de alguma matéria-prima (aço, madeira, etc.)
Então, eles inventaram bancos de dados de modelos de rede. Os bancos de dados do modelo de rede correspondem muito à herança múltipla. O C ++ suporta herança múltipla completamente, enquanto o Java suporta uma forma limitada (você pode herdar de apenas uma classe, mas também pode implementar quantas interfaces desejar).
Os bancos de dados de modelo hierárquico e de modelo de rede praticamente desapareceram do uso geral (embora alguns permaneçam em nichos bastante específicos). Para a maioria dos propósitos, eles foram substituídos por bancos de dados relacionais.
Muitos dos motivos pelos quais os bancos de dados relacionais assumiram o controle foram a versatilidade. O modelo relacional é funcionalmente um superconjunto do modelo de rede (que, por sua vez, é um superconjunto do modelo hierárquico).
C ++ seguiu amplamente o mesmo caminho. A correspondência entre herança única e o modelo hierárquico e entre herança múltipla e o modelo de rede é bastante óbvia. A correspondência entre os modelos C ++ e o modelo hierárquico pode ser menos óbvia, mas, de qualquer maneira, é bastante adequada.
Não vi uma prova formal disso, mas acredito que os recursos dos modelos são um superconjunto daqueles fornecidos por herança múltipla (que é claramente um superconjunto de herança única). A parte mais complicada é que os modelos são geralmente vinculados estaticamente - ou seja, toda a ligação acontece no tempo de compilação, não no tempo de execução. Como tal, uma prova formal de que a herança fornece um superconjunto das capacidades da herança pode muito bem ser um pouco difícil e complexa (ou até impossível).
De qualquer forma, acho que esse é o motivo real pelo qual o C ++ não usa herança para seus contêineres - não há motivo real para fazê-lo, porque a herança fornece apenas um subconjunto dos recursos fornecidos pelos modelos. Como os modelos são basicamente uma necessidade em alguns casos, eles também podem ser usados em quase todos os lugares.
fonte
Como você faz comparações com o ForwardIterator * 's? Ou seja, como você verifica se o item que possui é o que procura ou passou por ele?
Na maioria das vezes, eu usava algo assim:
o que significa que sei que estou apontando para o MyType e sei como compará-los. Embora pareça um modelo, na verdade não é (nenhuma palavra-chave "modelo").
fonte
Esta pergunta tem muitas ótimas respostas. Também deve ser mencionado que os modelos suportam um design aberto. Com o estado atual das linguagens de programação orientadas a objetos, é preciso usar o padrão de visitante ao lidar com esses problemas, e o OOP verdadeiro deve suportar várias ligações dinâmicas. Veja Open Multi-Methods para C ++, P. Pirkelbauer, et.al. para uma leitura muito interessante.
Outro ponto interessante dos modelos é que eles também podem ser usados no polimorfismo de tempo de execução. Por exemplo
Observe que essa função também funcionará se
Value
for algum vetor de algum tipo ( não std :: vector, que deve ser chamadostd::dynamic_array
para evitar confusão)Se
func
for pequeno, essa função ganhará muito com a inclusão. Exemplo de usoNesse caso, você deve saber a resposta exata (2,718 ...), mas é fácil construir um ODE simples sem solução elementar (Dica: use um polinômio em y).
Agora, você tem uma grande expressão
func
e usa o solucionador de ODE em muitos lugares, para que seu executável seja poluído com instanciações de modelo em todos os lugares. O que fazer? A primeira coisa a notar é que um ponteiro de função regular funciona. Então você deseja adicionar currying para escrever uma interface e uma instancia explícitaMas a instanciação acima funciona apenas para
double
, por que não escrever a interface como modelo:e especialize-se para alguns tipos de valores comuns:
Se a função tivesse sido projetada em torno de uma interface primeiro, você seria forçado a herdar desse ABC. Agora você tem essa opção, além de ponteiro de função, lambda ou qualquer outro objeto de função. A chave aqui é que precisamos ter
operator()()
, e devemos poder usar alguns operadores aritméticos em seu tipo de retorno. Assim, o mecanismo do modelo seria interrompido nesse caso se o C ++ não tivesse sobrecarga do operador.fonte
O conceito de separar interface de interface e poder trocar as implementações não é intrínseco à Programação Orientada a Objetos. Acredito que é uma ideia que surgiu no desenvolvimento baseado em componentes como o Microsoft COM. (Veja minha resposta em O que é desenvolvimento orientado a componentes?) Ao crescer e aprender C ++, as pessoas foram sensacionalistas quanto a herança e polimorfismo. Somente nos anos 90 as pessoas começaram a dizer "Programar para uma 'interface', não uma 'implementação'" e "Favorecer 'a composição de objetos' sobre 'herança de classe'". (ambos citados pelo GoF por sinal).
Em seguida, o Java veio junto com o coletor de lixo e a
interface
palavra - chave, e de repente tornou-se prático separar a interface e a implementação. Antes que você perceba, a ideia se tornou parte do OO. C ++, modelos e STL são anteriores a tudo isso.fonte