Por que o C ++ STL é tão fortemente baseado em modelos? (e não em * interfaces *)

211

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)

OB OB
fonte
4
Você pode conferir stackoverflow.com/questions/31693/… . A resposta aceita é uma excelente explicação de quais modelos oferecem sobre genéricos.
James McMahon
6
@Jonas: Isso não faz sentido. A restrição no cache custa ciclos de clock, e é por isso que é importante. No final do dia, são os ciclos de relógio, não o cache, que definem o desempenho. Memória e cache são importantes apenas na medida em que afetam os ciclos de clock gastos. Além disso, o experimento pode ser feito facilmente. Compare, digamos, std :: for_Each chamado com um argumento functor, com a abordagem OOP / vtable equivalente. A diferença no desempenho é impressionante . É por isso que a versão do modelo é usada.
jalf
7
e não há razão para que o código redundante esteja preenchendo o icache. Se eu instanciar o vetor <char> e o vetor <int> no meu programa, por que o código do <char> do vetor deve ser carregado no icache enquanto estou processando o vetor <int>? De fato, o código para o vetor <int> é aparado porque não precisa incluir código para conversão, vtables e indireto.
jalf
3
Alex Stepanov explica por que herança e igualdade não funcionam bem juntas.
fredoverflow
6
@BerndJendrissek: Uhm, feche, mas você não. Sim, mais custos de código em termos de largura de banda da memória e uso de cache, se realmente forem usados . Mas não há nenhuma razão particular para esperar um vector<int>e vector<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ção vector<int>que exija que o vector<char>código seja carregado ou executado.
jalf

Respostas:

607

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 vectorcabeçalho, sem também ser puxado iteratorou iostreampuxado? 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?

jalf
fonte
22
É uma boa redação, mas eu gostaria de destacar um detalhe. O STL não é um "produto" do C ++. De fato, o STL, como conceito, existia antes do C ++, e o C ++ era uma linguagem eficiente, com poder (quase) suficiente para programação genérica; portanto, o STL foi escrito em C ++.
Igor Krivokon
17
Como os comentários continuam aparecendo, sim, eu sei que o nome do STL é ambíguo. Mas não consigo pensar em um nome melhor para "a parte da biblioteca padrão C ++ que é modelada no STL". O nome de fato para essa parte da biblioteca padrão é apenas "o STL", mesmo que seja estritamente impreciso. :) Desde que as pessoas não usem STL como o nome para toda a biblioteca padrão (incluindo IOStreams e os cabeçalhos C stdlib), fico feliz. :)
jalf
5
@einpoklum E o que exatamente você ganha com uma classe base abstrata? Tome std::setcomo exemplo. Não herda de uma classe base abstrata. Como isso limita o seu uso std::set? Existe algo que você não pode fazer com um std::setporque ele não herda de uma classe base abstrata?
Fredoverflow 19/03/14
22
@einpoklum, dê uma olhada na linguagem Smalltalk, que Alan Kay projetou para ser uma linguagem OOP quando ele inventou o termo OOP. Não tinha interfaces. OOP não é sobre interfaces ou classes base abstratas. Você vai dizer que "Java, que não é nada parecido com o que o inventor do termo OOP tinha em mente, é mais OOP que C ++, que também não é nada parecido com o que o inventor do termo OOP tinha em mente"? O que você quer dizer é "C ++ não é como o Java o suficiente para o meu gosto". Isso é justo, mas não tem nada a ver com OOP.
jalf
8
@MasonWheeler se esta resposta foi um monte de bobagem flagrante você não veria literalmente centenas de desenvolvedores ao redor do mundo votação +1 nesta com apenas três pessoas a fazer o contrário
panda-34
88

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.

Tyler McHenry
fonte
4
"e com o C ++ 0x, algumas coisas podem ser programadas funcionalmente" - ele pode ser programado funcionalmente sem esses recursos, apenas de forma mais detalhada.
Jonas Kolker
3
@ Tyler De fato, se você restringisse C ++ a pura OOP, ficaria com Objective-C.
Justicle
@TylerMcHenry: Tendo acabado de perguntar isso , acho que acabei de proferir a mesma resposta que você! Apenas um ponto. Eu gostaria que você adicionasse o fato de que a Biblioteca Padrão não pode ser usada para escrever código Orientado a Objeto.
Einpoklum
74

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" :

Esse é o ponto fundamental: algoritmos são definidos em estruturas algébricas. Levei mais alguns anos para perceber que você precisa estender a noção de estrutura adicionando requisitos de complexidade a axiomas regulares. ... Eu acredito que as teorias de iteradores são tão centrais para a Ciência da Computação quanto as teorias de anéis ou espaços de Banach são centrais para a Matemática. Toda vez que eu olhava para um algoritmo, tentava encontrar uma estrutura na qual ele é definido. Então, o que eu queria fazer era descrever algoritmos genericamente. É o que eu gosto de fazer. Posso passar um mês trabalhando em um algoritmo conhecido tentando encontrar sua representação genérica. ...

STL, pelo menos para mim, representa a única maneira de programar. É, de fato, bem diferente da programação C ++, como foi apresentada e ainda é apresentada na maioria dos livros didáticos. Mas, veja bem, eu não estava tentando programar em C ++, estava tentando encontrar o caminho certo para lidar com o software. ...

Eu tive muitas partidas falsas. Por exemplo, passei anos tentando encontrar algum uso para herança e virtuais, antes de entender por que esse mecanismo era fundamentalmente falho e não deveria ser usado. Estou muito feliz que ninguém possa ver todas as etapas intermediárias - a maioria delas era muito boba.

(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):

O apoio de Bjarne Stroustrup foi crucial. Bjarne realmente queria STL no padrão e se Bjarne quer alguma coisa, ele consegue. ... Ele até me forçou a fazer mudanças no STL que eu nunca faria para mais ninguém ... ele é a pessoa mais decidida que eu conheço. Ele faz as coisas. Demorou um pouco para entender o que era o STL, mas quando o fez, estava preparado para fazer isso. Ele também contribuiu para a STL, defendendo a opinião de que mais de uma maneira de programação era válida - sem interferência e hype por mais de uma década, e buscando uma combinação de flexibilidade, eficiência, sobrecarga e segurança de tipo em modelos que tornaram possível o STL. Eu gostaria de declarar claramente que Bjarne é o designer de linguagem de destaque da minha geração.

Max Lybbert
fonte
2
Entrevista interessante. Tenho certeza de que já li isso há algum tempo, mas definitivamente valia a pena passar novamente. :)
jalf
3
Uma das entrevistas mais interessantes sobre programação que eu já li. Embora isso me deixa sedento de mais detalhes ...
Felixyz
Muitas das reclamações que ele faz sobre linguagens como Java ("Você não pode escrever um max () genérico em Java que usa dois argumentos de algum tipo e tem um valor de retorno desse mesmo tipo") eram relevantes apenas para versões muito antigas do idioma, antes da adição de genéricos. Mesmo desde o início, sabia-se que os genéricos seriam eventualmente adicionados, embora (uma vez que uma sintaxe / semântica viável fosse descoberta), então suas críticas são amplamente infundadas. Sim, de alguma forma os genéricos são necessários para preservar a segurança do tipo em uma linguagem estaticamente tipada, mas não, isso não torna o OO inútil.
Alguns Guy
1
@SomeGuy Eles não são reclamações sobre Java em si. Ele está falando sobre " a programação OO" padrão "do SmallTalk ou, digamos, do Java ". A entrevista é do final dos anos 90 (ele menciona trabalhar na SGI, que ele deixou em 2000 para trabalhar na AT&T). Os genéricos foram adicionados apenas ao Java em 2004 na versão 1.5 e são um desvio do modelo OO "padrão".
melpomene
24

A resposta é encontrada nesta entrevista com Stepanov, o autor do STL:

Sim. STL não é orientado a objetos. Eu acho que a orientação a objetos é quase tão falsa quanto a Inteligência Artificial. Ainda estou para ver um pedaço interessante de código que vem dessas pessoas OO.

StackedCrooked
fonte
Nice jóia; Você sabe de que ano é?
Kos
2
@Kos, de acordo com o web.archive.org/web/20000607205939/http://www.stlport.org/…, a primeira versão da página vinculada é de 7 de junho de 2001. A página em si mesma na parte inferior indica Copyright 2001- 2008.
AlfC20 /
@Kos Stepanov menciona trabalhar na SGI na primeira resposta. Ele deixou a SGI em maio de 2000, então presumivelmente a entrevista é mais antiga que isso.
precisa
18

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 :)

AraK
fonte
12
O Taj Mahal é pequeno e elegante, e sua casa é do tamanho de uma montanha e uma bagunça completa? ;)
jalf
Os conceitos não fazem mais parte do c ++ 0x. Algumas das mensagens de erro podem ser antecipadas usando static_asserttalvez.
KitsuneYMG
O GCC 4.6 melhorou as mensagens de erro do modelo e acredito que 4.7+ são ainda melhores com ele.
David Stone
Um conceito é essencialmente a "interface" que o OP estava solicitando. A única diferença é que a "herança" de um Conceito está implícita (se uma classe tem todas as funções de membro corretas, é automaticamente um subtipo do Conceito) em vez de explícita (uma classe Java deve declarar explicitamente que implementa uma interface) . No entanto, as subtipos implícito e explícito são OO válidas, e algumas linguagens OO têm herança implícita, que funciona exatamente como Conceitos. Então, o que está sendo dito aqui é basicamente "OO é péssimo: use modelos. Mas os modelos têm problemas, então use Conceitos (que são OO)".
Some Guy
11

os tipos de modelo devem seguir um "conceito" (Iterador de entrada, Iterador de avanço, etc ...) onde os detalhes reais do conceito são definidos inteiramente pela implementação da função / classe do modelo, e não pela classe do tipo usado com o modelo, que é um pouco anti-uso de OOP.

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.

você pode dizer à função ... espera um Iterador Direto apenas observando sua definição, onde seria necessário examinar a implementação ou a documentação para ...

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::copybase 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

Em genéricos, um argumento deve ser de uma classe derivada de uma interface (o equivalente em C ++ à interface é uma classe abstrata) especificada na definição do genérico. Isso significa que todos os tipos de argumentos genéricos devem caber em uma hierarquia. Isso impõe restrições desnecessárias aos projetos, requer uma previsão irracional por parte dos desenvolvedores. Por exemplo, se você escrever um genérico e eu definir uma classe, as pessoas não poderão usar minha classe como argumento para o seu genérico, a menos que eu conheça a interface que você especificou e tenha derivado minha classe dela. Isso é rígido.

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.

Steve Jessop
fonte
Nas interfaces explicitamente declaradas, duas palavras: digite classes. (Que já são o meio Stepanov por "conceito".)
pyon
"Se você não validar as expressões válidas necessárias para um conceito STL, você receberá um erro de compilação não especificado ao instanciar algum modelo com o tipo". Isso é falso. Passar algo para a stdbiblioteca que não corresponde a um conceito geralmente é "mal formado, não é necessário diagnóstico".
Yakk - Adam Nevraumont
É verdade que eu estava jogando rápido e solto com o termo "válido". Eu apenas quis dizer que se o compilador não puder compilar uma das expressões necessárias, ele informará algo.
21719 Steve Steveop
8

"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.

Ben Hughes
fonte
7

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.

Conte-nos algo sobre seu interesse a longo prazo em programação genérica

..... Então me ofereceram um emprego nos Laboratórios Bell que trabalhavam no grupo C ++ nas bibliotecas C ++. Eles me perguntaram se eu poderia fazer isso em C ++. Claro, eu não conhecia C ++ e, é claro, eu disse que podia. Mas não consegui fazê-lo em C ++, porque em 1987 o C ++ não possuía modelos, essenciais para ativar esse estilo de programação. A herança era o único mecanismo para obter a genéricos e não era suficiente.

Mesmo agora, a herança C ++ não é muito útil para programação genérica. Vamos discutir o porquê. Muitas pessoas tentaram usar herança para implementar estruturas de dados e classes de contêiner. Como sabemos agora, houve poucas tentativas bem-sucedidas. A herança de C ++ e o estilo de programação associado a ela são drasticamente limitados. É impossível implementar um design que inclua algo tão trivial quanto a igualdade em usá-lo. Se você começar com uma classe base X na raiz da sua hierarquia e definir um operador de igualdade virtual nessa classe que use um argumento do tipo X, derivará a classe Y da classe X. Qual é a interface da igualdade? Tem igualdade que compara Y com X. Usando animais como exemplo (as pessoas OO amam animais), defina mamíferos e obtenha girafas dos mamíferos. Em seguida, defina um companheiro de função de membro, onde o animal acasala com o animal e retorna um animal. Então você obtém a girafa do animal e, é claro, ele tem um companheiro de função em que a girafa se acasala com o animal e retorna um animal. Definitivamente, não é o que você quer. Embora o acasalamento possa não ser muito importante para programadores de C ++, a igualdade é. Não conheço um único algoritmo em que algum tipo de igualdade não seja usado.

yowkee
fonte
5

O problema básico com

void MyFunc(ForwardIterator *I);

é 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
1
Bem, eu também: 1. Não tente obtê-lo, pois estou escrevendo código genérico. Ou, 2. Obtenha usando qualquer mecanismo de reflexão que o C ++ ofereça atualmente.
Einpoklum
2

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.

Jerry Coffin
fonte
0

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:

void MyFunc(ForwardIterator<MyType>& i)

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").

Tanktalus
fonte
você pode apenas usar os botões <,> e = operadores do tipo e não sabe o que são esses (embora isso possa não ser o que você quis dizer)
lhahne
Dependendo do contexto, esses podem não fazer sentido ou podem funcionar bem. Difícil dizer sem saber mais sobre o MyType, o que, presumivelmente, o usuário sabe, e nós não.
Tanktalus
0

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

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Observe que essa função também funcionará se Valuefor algum vetor de algum tipo ( não std :: vector, que deve ser chamado std::dynamic_arraypara evitar confusão)

Se funcfor pequeno, essa função ganhará muito com a inclusão. Exemplo de uso

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

Nesse 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 funce 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ícita

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Mas a instanciação acima funciona apenas para double, por que não escrever a interface como modelo:

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

e especialize-se para alguns tipos de valores comuns:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

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.

user877329
fonte
-1

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 interfacepalavra - 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.

Eugene Yokota
fonte
Concordou que as interfaces não são apenas OO. Mas o polimorfismo de capacidade no sistema de tipos é (foi em Simula nos anos 60). As interfaces de módulo existiam no Modula-2 e no Ada, mas essas operavam no sistema de tipos de maneira diferente, eu acho.
precisa saber é o seguinte