Por que os conceitos (programação genérica) foram concebidos quando já tínhamos classes e interfaces?

8

Também no stackoverflow.com :

Entendo que os conceitos de STL precisavam existir e que seria tolo chamá-los de "classes" ou "interfaces" quando, na verdade, são apenas conceitos documentados (humanos) e não puderam ser traduzidos para o código C ++ no momento, mas quando tiveram a oportunidade de estender a linguagem para acomodar conceitos, por que eles simplesmente não modificaram os recursos das classes e / ou interfaces introduzidas?

Não é um conceito muito semelhante a uma interface (classe 100% abstrata sem dados)? Olhando para ele, parece-me que as interfaces apenas carecem de suporte para axiomas, mas talvez axiomas possam ser introduzidos nas interfaces do C ++ (considerando uma adoção hipotética de interfaces no C ++ para assumir conceitos), não poderiam? Acho que mesmo conceitos automáticos poderiam ser facilmente adicionados a uma interface C ++ (interface automática LessThanComparable, alguém?).

Um concept_map não é muito semelhante ao padrão do adaptador? Se todos os métodos estiverem em linha, o adaptador basicamente não existe além do tempo de compilação; o compilador simplesmente substitui as chamadas para a interface pelas versões incorporadas, chamando o objeto de destino diretamente durante o tempo de execução.

Ouvi falar de algo chamado Programação Estática Orientada a Objetos, que essencialmente significa reutilizar efetivamente os conceitos de orientação a objetos em programação genérica, permitindo assim o uso da maior parte do poder do OOP sem incorrer em sobrecarga de execução. Por que essa idéia não foi mais considerada?

Espero que isso esteja claro o suficiente. Eu posso reescrever isso se você acha que eu não estava; apenas me avise.

Gui Prá
fonte

Respostas:

13

Resposta curta: você está misturando conceitos antes do tempo de compilação e tempo de compilação que têm semelhanças em seus propósitos. As interfaces (classes abstratas e toda a implementação do paradigma de orientação a objetos) são forçadas no momento da compilação . Os conceitos são a mesma idéia, mas no contexto da programação genérica que ocorre em C ++ ANTES do tempo de compilação . Ainda não temos esse último recurso.

Mas deixe-me explicar desde o início.


Resposta longa:

De fato, os Conceitos são apenas aplicação de linguagem e "facilitado para o programador" de algo já presente na linguagem, que você poderia chamar de "digitação de pato".

Quando você passa um tipo para uma função de modelo, que é uma função genérica a partir da qual o compilador gera código real (em linha) quando chamado, esse tipo precisa ter algumas propriedades (características?) Que serão usadas no código do modelo. Portanto, é a idéia de digitar patos, mas tudo é gerado e feito em tempo de compilação .

O que acontece quando o tipo não possui as propriedades necessárias?

Bem, o compilador saberá que há um problema apenas quando o código gerado do modelo for compilado e falhar. Isso significa que o erro que será gerado será um erro dentro do código do modelo, que será mostrado ao programador como seu erro. Além disso, o erro terá toneladas de informações por causa das meta-informações fornecidas no caso de geração de código do modelo, para saber de qual instanciação do modelo estamos falando.

Vários problemas com isso: primeiro, na maioria das vezes, o código de modelo é código de biblioteca e a maioria dos programadores são usuários de código de biblioteca, não criadores de código de biblioteca. Isso significa que esse tipo de erro enigmático é realmente difícil de entender quando você não entende como a biblioteca é escrita (não apenas o design, como é realmente implementada). O segundo problema é que, mesmo quando o programador escreveu o código do modelo, os motivos da falha ainda podem ser obscuros porque o compilador poderá dizer que há um problema tarde demais: quando o código gerado está sendo compilado. Se o problema for relativo às propriedades do tipo , verifique-o antes mesmo de gerar o código.

É para isso que os Conceitos permitem (e são projetados): permitir que o programador (código genérico) especifique as propriedades dos tipos que são passados ​​como parâmetros do modelo e, em seguida, permita que o compilador forneça erros explícitos, caso os tipos fornecidos não atendam às requisitos.

Quando a verificação for bem-sucedida, o código será gerado a partir do modelo e então compilado, certamente com sucesso.

Toda a verificação do conceito ocorre exclusivamente antes do tempo de compilação . Ele verifica os próprios tipos, não os tipos de objeto . Não há objeto antes do tempo de compilação.

Agora, sobre "interfaces".

Ao criar um tipo base abstrato ou virtual, você permite que o código o utilize para manipular objetos dos tipos filhos sem conhecer suas implementações reais. Para impor isso, o tipo base expõe membros que são virtuais e podem ser (ou precisam ser) sobrecarregados pelos tipos filhos.

Isso significa que o compilador pode verificar em tempo de compilação que todos os objetos passados ​​para uma função que requer uma referência à classe base devem ser 1. de um dos tipos filhos da classe base, 2. esse tipo filho deve ter implementações de funções puras virtuais declaradas nas classes base, se houver.

Portanto, no tempo de compilação , o compilador verifica as interfaces dos tipos de objeto e informa se algo está faltando.

É a mesma ideia que Concepts, mas ocorre tarde demais , como foi dito na descrição do Concept. Ocorre no tempo de compilação. Não estamos no código genérico (código do modelo), estamos depois que ele foi processado e já é tarde demais para verificar se os tipos atendem aos requisitos genéricos, que não podem ser expostos pelas classes base virtuais. De fato, toda a implementação do paradigma de orientação a objetos em C ++ nem existe quando o código do modelo está sendo processado. Ainda não há objetos. Isso é

As classes descrevem restrições nos objetos a serem usados ​​para verificar os requisitos de funções que manipulam esses objetos. Os conceitos descrevem restrições nos tipos (incluindo classes) a serem usadas para verificar os requisitos de código genérico para gerar código real a partir desses tipos e combinação de código genérico.

Então, novamente, é o mesmo "teste de sanidade", mas em outra camada da linguagem, ou seja, modelos. Os modelos são uma linguagem completa (turing complete) que permite metaprogramação e tipos de programação mesmo antes de aparecerem no código compilado. É um pouco como criar scripts para o compilador. Digamos que você possa criar um script, classes são apenas valores manipulados pelo script. Atualmente, não há como verificar restrições nesses valores além de travar o script de uma maneira não óbvia. Os conceitos são apenas isso: forneça a digitação nesses valores (que no código gerado são tipos). Não tenho certeza se estou claro ...

Outra diferença realmente importante entre as classes base virtuais e os Conceitos é que a primeira força uma forte relação entre os tipos, tornando-os "ligados pelo sangue". Enquanto a metaprogramação de modelos permite "digitação de pato", os Conceitos apenas permitem tornar os requisitos mais claros.

Klaim
fonte
4

Declarar uma classe é " objetos de programação ".
Declarar um conceito é " classes de programação ".

Obviamente, como sempre a programação é, certas analogias podem ser vistas, mas as duas coisas pertencem a uma fase diferente do processo de abstração. Basicamente, uma "classe" (e tudo ao seu redor, como "interfaces") diz ao compilador como estruturar os objetos que a máquina executora instanciará em tempo de execução. Um "conceito" tem como objetivo informar ao compilador como uma "classe" deve ser estruturada para ser "compilável" em um determinado contexto.

Obviamente, é teoricamente possível reiterar essas etapas repetidas vezes

  • objetos
  • tipos de objetos ( classes )
  • tipos de (tipos de objetos) ( conceitos )
  • tipos de (tipos de (tipos de objetos)) ( ??? )
  • .....

No momento, "conceitos" foram descartados pelas especificações do C ++ 0x (uma vez que ainda exigem algum trabalho e foram retidos, não era mais o caso de adiar). A idéia do conceito n não sei - agora - se pode ser sempre útil.

Emilio Garavaglia
fonte
Isso faz muito sentido conceitualmente; Estou tentando entender e pensar se isso responde à pergunta. Muito obrigado.
Gui Prá
3

A resposta simples para praticamente todas as suas perguntas é "Porque os compiladores C ++ são ruins". A sério. Eles são construídos com a tecnologia da unidade de tradução da C, que proíbe efetivamente muitas coisas úteis, e as implementações de modelos existentes são terrivelmente lentas. Os conceitos não foram cortados por qualquer motivo conceitual - eles foram cortados porque não havia uma implementação confiável, o ConceptGCC era extremamente lento e a especificação de conceitos levou um tempo absurdamente longo. Herb Sutter afirmou que era necessário mais espaço para especificar os conceitos usados ​​na biblioteca Padrão do que para especificar o conjunto de modelos.

Indiscutivelmente, entre SFINAE decltypee static_assert, eles são na maioria implementáveis, como é agora.

DeadMG
fonte
2

No meu entendimento, Interfaces e Conceitos têm propósitos semelhantes em diferentes partes da linguagem C ++.

Conforme mencionado na resposta à pergunta original: A implementação de uma interface é decidida pelo implementador de uma classe no tempo de design. Depois que uma classe é publicada, ela pode suportar apenas as interfaces das quais foi derivada no momento do design.

Duas interfaces distintas com exatamente as mesmas funções-membro e semântica (ou seja, o mesmo conceito) ainda serão duas interfaces distintas. Se você deseja dar suporte à semântica de ambas as interfaces, pode ser necessário implementar o suporte duas vezes.

Esse é o problema que a Programação Genérica pretende solucionar. Na Programação Genérica em C ++, o tipo passado para um modelo simplesmente precisa oferecer suporte à interface (sem maiúsculas, no sentido da "interface de programação" de um tipo) usada pelo modelo. As duas interfaces distintas com as mesmas funções de membro corresponderão e você precisará escrever o código apenas uma vez. Além disso, quaisquer tipos (mesmo sem interfaces explícitas) compatíveis com a mesma interface funcionarão.

Isso leva a uma segunda questão: e se você tiver dois tipos com interfaces sobrepostas, mas semânticas diferentes ? A programação genérica não será capaz de dizer a diferença e tudo será compilado muito bem, mas o resultado do tempo de execução será surpreendente e provavelmente errado.

É aí que entram os conceitos. Se você simplificar demais, pode considerar um conceito como a versão genérica (modelo) de uma interface. Ele precisa ser implementado apenas uma vez para aplicar a um grande número de tipos potenciais que podem "derivar" do Conceito. Um conceito é uma interface semântica predeterminada de um tipo (classe) que não se limita apenas a esse tipo. É diferente de uma Interface, pois dois tipos muito diferentes (para o compilador genérico) ainda podem ter o mesmo Conceito, sem ter que recorrer à limitação de Interfaces da classe base, ou Interfaces da classe base que não possuem semântica real visível do compilador. próprias, mas são usadas apenas para distinção de tipos.

Joris Timmermans
fonte
Espere ... Você disse que os conceitos são implementados uma vez e se aplicam a um grande número de tipos potenciais que podem "derivar" dele. Isso significa que todos os tipos potenciais precisam derivar disso. Se dois conceitos com o mesmo conteúdo estiverem presentes em duas bibliotecas diferentes, sem mapeamento automático, você terá o mesmo problema que as interfaces. Ao mesmo tempo, uma interface pode apresentar mapeamento automático. Os problemas parecem iguais para mim.
Gui Prá
1
@ n2liquid - Os conceitos são uma combinação diferente de benefícios e desvantagens entre interfaces e programação genérica pura. Eles não são uma melhoria estrita. Os conceitos não evitam a parte "predeterminada" das interfaces. Os conceitos evitam a situação em que os tipos de classe suportam a mesma semântica, mas não podem derivar da mesma interface (por exemplo, onde a interface é definida para o subtipo duplo, enquanto a versão genérica se aplica a todos os tipos numéricos).
Joris Timmermans