O que as pessoas fizeram antes dos modelos em C ++? [duplicado]

42

Eu não sou novo em programação, mas eu sou um que começou há alguns anos e eu amo modelos.

Mas antes, como as pessoas lidavam com situações em que precisavam de geração de código em tempo de compilação como modelos? Eu estou supondo horrível, macros horríveis (pelo menos é como eu faria isso), mas pesquisar a questão acima só me traz páginas e páginas de tutoriais de modelos.

Existem muitos argumentos contra o uso de modelos e, embora normalmente se reduz à legibilidade, " YAGNI ", e reclamando sobre quão mal ele é implementado, não há muito sobre as alternativas com poder similar. Quando eu faço preciso fazer algum tipo de genéricos em tempo de compilação e fazer quero manter meu código DRY , como evita-se o uso de modelos?

    
por IdeaHat 12.11.2014 / 16:56
fonte

9 respostas

48

Além do ponteiro void *, que é coberto pela na resposta de Robert , uma técnica como essa foi usada (Aviso: Memória de 20 anos):

#define WANTIMP

#define TYPE int
#include "collection.h"
#undef TYPE

#define TYPE string
#include "collection.h"
#undef TYPE

int main() {
    Collection_int lstInt;
    Collection_string lstString;
}

Onde eu esqueci a mágica exata do pré-processador dentro de collection.h, mas era algo assim:

class Collection_ ## TYPE {
public:
 Collection() {}
 void Add(TYPE value);
private:
 TYPE *list;
 size_t n;
 size_t a;
}

#ifdef WANTIMP
void Collection_ ## TYPE ::Add(TYPE value)
#endif
    
por 12.11.2014 / 20:13
fonte
44

A maneira tradicional de implementar genéricos sem ter genéricos (os modelos de motivos foram criados) é usar um ponteiro vazio.

typedef struct Item{ 
        void* data;
    } Item;

typedef struct Node{
    Item Item; 
    struct Node* next; 
    struct Node* previous;
} Node;

Neste código de exemplo, uma árvore binária ou uma lista duplamente vinculada pode ser representada. Como item encapsula um ponteiro vazio, qualquer tipo de dados pode ser armazenado lá. Claro, você teria que saber o tipo de dados em tempo de execução para que você possa converter de volta para um objeto utilizável.

    
por 12.11.2014 / 17:21
fonte
16

Como outras respostas apontadas, você pode usar void* para estruturas de dados genéricas. Para outros tipos de polimorfismo paramétrico, as macros de pré-processador eram usadas se algo se repetisse em um lote (como dezenas de vezes). Para ser honesto, na maioria das vezes, para repetição moderada, as pessoas apenas copiaram e colaram, depois mudaram os tipos, porque há muitas armadilhas com macros que as tornam problemáticas.

Nós realmente precisamos de um nome para o inverso do paradoxo do blub , onde as pessoas têm dificuldade em imaginar a programação em um linguagem menos expressiva, porque isso aparece muito neste site. Se você nunca usou uma linguagem com formas expressivas de implementar polimorfismo paramétrico, você realmente não sabe o que está perdendo. Você simplesmente aceita a cópia e a colagem como algo irritante, mas necessário.

Existem ineficiências em seus idiomas atuais que você nem conhece ainda. Em vinte anos, as pessoas ficarão imaginando como você as eliminou. A resposta curta é que você não fez, porque você não sabia que podia.

    
por 12.11.2014 / 18:07
fonte
8

Lembro quando o gcc foi enviado com genclass - um programa que recebeu como entrada um conjunto de tipos de parâmetros (por exemplo, chave e valor para um Mapa) e um arquivo de sintaxe especial que descrevia um tipo parametrizado (digamos, um Mapa ou um Vector) e gerou uma implementação válida em C ++ com os tipos de parâmetros preenchidos.

Então, se você precisasse de Map<int, string> e Map<string, string> (essa não era a sintaxe real, lembre-se disso) você tinha que rodar esse programa duas vezes para gerar algo como map_string_string.h e map_int_string.h e então usá-los em seu código .

Esta é a página man do genclass : link

    
por 13.11.2014 / 12:26
fonte
7

[Para o OP: Eu não estou tentando pegá-lo pessoalmente, mas aumentar a consciência de seus e de outros sobre a lógica da (s) pergunta (s) feita (s) no SE e em outros lugares. Por favor, não leve isso para o lado pessoal!]

O título da pergunta é bom, mas você está limitando severamente o escopo de suas respostas, incluindo '... situações em que eles precisavam de geração de código em tempo de compilação'. Muitas boas respostas para a pergunta sobre como fazer a geração de código em tempo de compilação em C ++ sem templates existem nesta página, mas para responder à pergunta que você originalmente fez:

O que as pessoas faziam antes dos modelos em C ++?

A resposta é, claro, eles (nós) não os usamos. Sim, estou sendo irônico, mas os detalhes da questão no corpo parecem (talvez exageradamente) supor que todo mundo adora modelos e que nenhuma codificação poderia ter sido feita sem eles.

Como exemplo, eu completei muitos projetos de codificação em vários idiomas sem precisar de geração de código em tempo de compilação, e acredito que outros também. Claro, o problema resolvido pelos modelos era uma coceira grande o suficiente para que alguém realmente o arranhasse, mas o cenário proposto por essa questão era, em grande parte, inexistente.

Considere uma pergunta semelhante em carros:

Como os motoristas mudaram de uma engrenagem para outra, usando um método automatizado que mudou as marchas para você antes que a transmissão automática fosse inventada?

A pergunta é, claro, boba. Perguntar como uma pessoa fez X antes de X ser inventado não é realmente uma pergunta válida. A resposta é geralmente 'nós não fizemos e não sentimos falta porque não sabíamos que ele existiria'. Sim, é fácil ver o benefício após o fato, mas presumir que todos estavam por perto, chutando os pés, esperando pela transmissão automática ou modelos C ++, não é verdade.

À pergunta: "como os motoristas mudaram de marcha antes que a transmissão automática fosse inventada?" pode-se razoavelmente responder "manualmente", e esse é o tipo de respostas que você está obtendo aqui. Pode até ser o tipo de pergunta que você queria perguntar.

Mas não foi esse que você perguntou.

Então:

P: Como as pessoas usaram modelos antes de os modelos serem inventados?

A: Nós não o fizemos.

P: Como as pessoas usavam modelos antes de os modelos serem inventados, quando eles precisavam usar modelos ?

A: Nós não precisamos usá-los. Por que assumimos que fizemos? (Por que assumimos que fazemos?)

P: Quais são as formas alternativas de alcançar os resultados que os modelos fornecem?

A: Muitas boas respostas existem acima.

Por favor, pense em falácias lógicas em seus posts antes de postar.

[Obrigado! Por favor, nenhum dano pretendido aqui.]

    
por 13.11.2014 / 01:27
fonte
6

Macros horríveis estão certas, a partir do link :

Bjarne Stroustrup: Yes. When you say, "template type T," that is really the old mathematical, "for all T." That's the way it's considered. My very first paper on "C with Classes" (that evolved into C++) from 1981 mentioned parameterized types. There, I got the problem right, but I got the solution totally wrong. I explained how you can parameterize types with macros, and boy that was lousy code.

Você pode ver como uma versão antiga de macro de um modelo foi usada aqui: link

    
por 12.11.2014 / 19:10
fonte
5

Como Robert Harvey já disse, um ponteiro vazio é o tipo de dados genérico.

Um exemplo da biblioteca C padrão, como classificar uma matriz de double com uma classificação genérica:

double *array = ...;
int size = ...;   
qsort (array, size, sizeof (double), compare_doubles);

Em que compare_double é definido como:

int compare_doubles (const void *a, const void *b)
{
    const double *da = (const double *) a;
    const double *db = (const double *) b;
    return (*da > *db) - (*da < *db);
}

A assinatura de qsort está definida em stdlib.h:

void qsort(void *base, size_t nmemb, size_t size,
    int (*compar)(const void *, const void *)
);

Observe que não há verificação de tipo no tempo de compilação, nem mesmo no tempo de execução. Se você classificar uma lista de strings com o comparador acima que espera o dobro, ele tentará interpretar a representação binária de uma string como um double e ordenar de acordo.

    
por 12.11.2014 / 17:44
fonte
1

Uma maneira de fazer isso é assim:

link

#define DARRAY_DEFINE(name, type) DARRAY_TYPEDECL(name, type) DARRAY_IMPL(name, type)

// This is one single long line
#define DARRAY_TYPEDECL(name, type) \
typedef struct darray_##name \
{ \
    type* base; \
    size_t allocated_mem; \
    size_t length; \
} darray_##name; 

// This is also a single line
#define DARRAY_IMPL(name, type) \
static darray_##name* darray_init_##name() \
{ \
    darray_##name* arr = (darray_##name*) malloc(sizeof(darray_##name)); \
    arr->base = (type*) malloc(sizeof(type)); \
    arr->length = 0; \
    arr->allocated_mem = 1; \
    return arr; \
}

A macro DARRAY_TYPEDECL cria efetivamente uma definição de estrutura (em uma única linha), substituindo name pelo nome passado e armazenando uma matriz do type que você passa (o name está lá para que você possa concatená-lo ao nome da estrutura base e ainda ter um identificador válido - darray_int * não é um nome válido para uma struct), enquanto a macro DARRAY_IMPL define as funções que operam nessa struct (nesse caso, elas são marcadas como static apenas para que um só chamaria a definição uma vez e não separaria tudo).

Isso seria usado como:

#include "darray.h"
// No types have been defined yet
DARRAY_DEFINE(int_ptr, int*)

// by this point, the type has been declared and its functions defined
darray_int_ptr* darray = darray_int_ptr_init();
    
por 14.11.2014 / 14:40
fonte
1

Acho que os modelos são muito usados como uma maneira de reutilizar tipos de contêiner com muitos valores algorítmicos, como matrizes dinâmicas (vetores), mapas, árvores, etc., etc.

Sem modelos, necessariamente, esses contêm implementações são escritas de forma genérica e são fornecidas apenas informações suficientes sobre o tipo necessário para seu domínio. Por exemplo, com um vetor, eles precisam apenas que os dados sejam legíveis e precisam saber o tamanho de cada item.

Digamos que você tenha uma classe de contêiner chamada Vector que faz isso. Leva void *. O uso simplista disso seria que o código da camada de aplicativo fizesse muita conversão. Então, se eles estão gerenciando objetos Cat, eles têm que conjurar Cat * para void * e de volta para todo o lugar. Lixeira de código de aplicação com elencos tem problemas óbvios.

Modelos resolvem isso.

Outra maneira de resolvê-lo é criar um tipo de contêiner personalizado para o tipo que você está armazenando no contêiner. Então, se você tem uma classe Cat, você cria uma classe CatList derivada de Vector. Você então sobrecarrega os poucos métodos que você usa, introduzindo versões que levam objetos Cat ao invés de void *. Então você sobrecarregaria o método Vector :: Add (void *) com Cat :: Add (Cat *), que internamente simplesmente passa o parâmetro para Vector :: Add (). Em seguida, no código do seu aplicativo, você chamaria a versão sobrecarregada de Adicionar ao passar um objeto Cat e, assim, evitar a transmissão. Para ser justo, o método Add não exige um lançamento porque um objeto Cat * é convertido em void * sem conversão. Mas o método para recuperar um item, como a sobrecarga de índice ou um método Get () seria.

A outra abordagem, o único exemplo de que me lembro a partir do início dos anos 90 com uma estrutura de aplicativo grande, é usar um utilitário personalizado que cria esses tipos sobre as classes. Eu acredito que o MFC fez isso. Eles tinham classes diferentes para contêineres como CStringArray, CRectArray, CDoulbeArray, CIntArray, etc. Em vez de manter código duplicado, eles faziam algum tipo de meta-programação semelhante a macros, usando uma ferramenta externa que geraria as classes. Eles disponibilizaram a ferramenta com o Visual C ++ para o caso de alguém querer usá-la - eu nunca usei. Talvez eu deveria ter. Mas, na época, os especialistas estavam divulgando "Subconjunto de Céi" e "Você não precisa de modelos"

    
por 12.11.2014 / 18:23
fonte