Facilitador para implementar um tipo de polimorfismo em C

119 views
Skip to first unread message

Thiago Adams

unread,
Apr 25, 2017, 8:16:23 PM4/25/17
to ccppbrasil

Fiz um gerador de "casts" e funcoes "virtuais"para criar um tipo de polimorfismo em C.
O tipo de polimorfismo usa um id intrusivo por instancia.
Existem ponteiros para os tipos que pode apontar para diferentes tipos concretos.
A pagina carrega um exemplo simples.


O trabalho do gerador eh explodir os tipos e achar as instancias criando os possiveis  casts e tambem 
um exemplo de chamada de funcao polimorfica.
Estes casts sao defines que alem de fazer o cast deixam o fonte mais seguro por nao usar nenhum cast foçado.
os defines guiam o programador para os casts permitidos.

Para usar se cria um header com os ID e outro com as definicoes de funcoes polimorficas e um c com a imroplementacao.
depois eu quero melhor o gerador, por enquanto ele demonstra  a ideia.


        {
        "Circle" : [],
        "Rectangle" : [],
        "Shape" : ["Circle", "Rectangle"]
        }
    
Circle e Retangle sao tipo concretos existentes.
Shape eh um ponteiro para Circle ou Rectangle.

Para um exemplo maior, e ver como pode ser "mala" fazer na mao.

{
    "TDeclaration" : [],
    "TStaticAssertDeclaration" : [],
    "TSingleTypeSpecifier" : [],
    "TEnumSpecifier" : [],
    "TStructUnionSpecifier" : [],
    "TStructDeclaration" : [],
    "TCompoundStatement" : [],
    "TExpressionStatement" : [],
    "TSwitchStatement" : [],
    "TLabeledStatement" : [],
    "TForStatement" : [],
    "TJumpStatement" : [],
    "TAsmStatement" : [],
    "TWhileStatement" : [],
    "TDoStatement" : [],
    "TIfStatement" : [],
    "TReturnStatement" : [],
    "TInitializerListType" : [],
    "TPrimaryExpression" : [],
    "TUnaryExpressionOperator" : [],
    "TCastExpressionType" : [],
    "TPrimaryExpressionValue" : [],
    "TPostfixExpressionCore" : [],
    "TBinaryExpression" : [],
    "TTernaryExpression" : [],
    "TExpression2": [],

    "TStatement" : ["TCompoundStatement",
      "TExpressionStatement", 
      "TLabeledStatement", 
      "TJumpStatement", 
      "TIfStatement", 
      "TDoStatement", 
      "TForStatement", 
      "TAsmStatement", 
      "TReturnStatement", 
      "TWhileStatement", 
      "TSwitchStatement"],


    "TAnyStructDeclaration" : ["TStructDeclaration", "TStaticAssertDeclaration"],

    "TTypeSpecifier" : ["TSingleTypeSpecifier", "TEnumSpecifier", "TStructUnionSpecifier"],

    "TAnyDeclaration" : [],

    "TBlockItem" : ["TStatement", "TAnyDeclaration"],
    "TInitializer": ["TInitializerListType", "TExpression2"]
}
Que era o meu exemplo motivador.

Thiago Sonego Goulart

unread,
Apr 26, 2017, 2:06:08 AM4/26/17
to ccppb...@googlegroups.com
Algum motivo em particular pra preferir essa abordagem ao invés de algo mais similar a uma vtable na classe abstrata? Na minha opinião, a saída do teu gerador é complexa demais, eu odiaria fazer manutenção nesse código.

Recentemente, tive que modificar uma biblioteca escrita em C para adicionar suporte a membros genéricos a algumas structs. Minha solução foi fazer com que todos os tipos concretos que eu quis suportar "herdassem" uma struct abstrata (teu gerador não mostra a definição das estruturas concretas, mas imagino que ambos usamos a mesma estratégia pra isso), e essa estrutura abstrata guardava um ponteiro pra uma função "polimórfica". Isso significa que todos os "objetos" devem ser criados a partir de uma única função que inicializa os ponteiros corretamente, mas essa também é uma limitação da tua abordagem por causa dos IDs, a diferença principal é que não é preciso criar uma função genérica com um switch gigantesco pra cada uma das funções que tu quer implementar. Minha abordagem para conversões também foi mais simples, mas porque eu não me preocupei com type safety (nada vai impedir alguém de dar um cast pra um tipo incompatível ao invés de usar as macros).

Me perdoe se minha explicação foi abstrata demais, se quiser um exemplo eu preciso de um pouco mais de tempo pra preparar.

--
Thiago Sonego Goulart

--
Antes de enviar um e-mail para o grupo leia:
http://www.ccppbrasil.org/wiki/Lista:AntesdePerguntar
--~--~---------~--~----~------------------------------
[&] C & C++ Brasil - http://www.ccppbrasil.org/
Para sair dessa lista, envie um e-mail para ccppbrasil-unsubscribe@googlegroups.com
Para mais opções, visite http://groups.google.com/group/ccppbrasil
--~--~---------~--~----~--~-~--~---~----~------------

Thiago Adams

unread,
Apr 26, 2017, 8:26:14 AM4/26/17
to ccppbrasil


On Wednesday, April 26, 2017 at 3:06:08 AM UTC-3, Thiago Sonego Goulart wrote:
Algum motivo em particular pra preferir essa abordagem ao invés de algo mais similar a uma vtable na classe abstrata?

O caso de uso foi criar tipos para uma AST. 
Na AST a maioria dos algorítmos são externos ao tipo. (Ele não sabe como será usado)
As virtuais não ajudam e algumas pessoas criam visitors neste caso quando implementam 
com virtuais.
No geral, quando é possível, eu prefiro este modelo. Acho que a classificação dos tipos fica
melhor fora.

 
Na minha opinião, a saída do teu gerador é complexa demais, eu odiaria fazer manutenção nesse código.
A saída é um switch simples e não existe necessidade de editar. Estou pensando em fazer um experimento
para extender a linguagem C com este gerador. A criação da funcao switch seria parecido com a instanciacao
do template do C++. Ou seja, ao usar a função ela é instanciada.
 

Recentemente, tive que modificar uma biblioteca escrita em C para adicionar suporte a membros genéricos a algumas structs. Minha solução foi fazer com que todos os tipos concretos que eu quis suportar "herdassem" uma struct abstrata (teu gerador não mostra a definição das estruturas concretas, mas imagino que ambos usamos a mesma estratégia pra isso), e essa estrutura abstrata guardava um ponteiro pra uma função "polimórfica". Isso significa que todos os "objetos" devem ser criados a partir de uma única função que inicializa os ponteiros corretamente, mas essa também é uma limitação da tua abordagem por causa dos IDs, a diferença principal é que não é preciso criar uma função genérica com um switch gigantesco pra cada uma das funções que tu quer implementar. Minha abordagem para conversões também foi mais simples, mas porque eu não me preocupei com type safety (nada vai impedir alguém de dar um cast pra um tipo incompatível ao invés de usar as macros).

Vou colocar em exemplo completo embaixo para ficar claro.
O  C++ é mais bonzinho em avisar sobre casts errados. Porém o C não vai avisar nada.
Como eu posso dizer externamente ao tipo se ele é isso ou aquilo, ao mudar esta definição eu teria que revisar os casts. Com os defines, ao mudar qualquer coisa, vai dar erro de macro não existente se não for compatível com a definição.


 
Me perdoe se minha explicação foi abstrata demais, se quiser um exemplo eu preciso de um pouco mais de tempo pra preparar.

 Entendi que você tem a vtable no objeto. 

 Exemplo completo:

Para a definição:
{
  "Circle" : [],
  "Rectangle" : [],
  "Shape" : ["Circle", "Rectangle"]
}

=== types.h - gerado ===
#pragma once
#define CIRCLE_ID 0
#define RECTANGLE_ID 1
   
=== Circle.h ===

#pragma once
#include "types.h"

typedef struct
{
  int id;
  int radius;
} Circle;

#define CIRCLE_INIT (Circle) { CIRCLE_ID, 0}

void Circle_Draw(Circle*p);

=== Circle.c ===

#include "Circle.h"
void Circle_Draw(Circle*p) {}

=== Retangle.h ==

#pragma once
#include "types.h"

typedef struct
{
  int id;
  int width;
} Rectangle;

#define RECTANGLE_INIT (Rectangle) { RECTANGLE_ID, 0}

void Rectangle_Draw(Rectangle*p);

=== Rectangle.c ===

#include "Rectangle.h"
void Rectangle_Draw(Rectangle*p) {}

=== Shape.h - gerado ===

#pragma once

#include "Circle.h"
#include "Rectangle.h"

typedef struct { int id; } Shape;

/*casts for Shape*/

#define Shape_As_Circle(p)  ((p)->id == CIRCLE_ID ? (Circle*) p : 0)
#define Circle_As_Shape(p) (p)

#define Shape_As_Rectangle(p)  ((p)->id == RECTANGLE_ID ? (Rectangle*) p : 0)
#define Rectangle_As_Shape(p) (p)


void Shape_Draw(Shape* p);

=== Shape.c - gerado ===

#include "Shape.h"
void Shape_Draw(Shape* p)
{
  switch (p->id)
  {
  case CIRCLE_ID:
    Circle_Draw((Circle*)p);
    break;

  case RECTANGLE_ID:
    Rectangle_Draw((Rectangle*)p);
    break;

  }
}

=== sample.c ===

#include <stdio.h>
#include "Shape.h"

int main()
{
  Circle* p = malloc(sizeof * p);
  *p = CIRCLE_INIT;

  Shape* pShape = Circle_As_Shape(p);
  Shape_Draw(pShape);
  return 0;
}

Virgilio Fornazin

unread,
Apr 26, 2017, 11:46:26 AM4/26/17
to ccppb...@googlegroups.com
e pq nao usar "C + ou -" (ou C com classes) ao inves disso?

Thiago Adams

unread,
Apr 26, 2017, 12:24:30 PM4/26/17
to ccppbrasil


On Wednesday, April 26, 2017 at 12:46:26 PM UTC-3, Virgilio Fornazin wrote:
e pq nao usar "C + ou -" (ou C com classes) ao inves disso?


Não sei se você quer dizer no caso genérico ou específico. 
No meu caso especifico é um parser de C escrito em C. Assim eu posso
passar o parser nele mesmo. Isso é um motivo para ele ser em C.

No caso genérico, a questão é avaliar o tipo de implementação de polimorfismo
desejada. Existem várias opções, incluindo a clássica por vtable intrusiva que vem
na linguagem C++, juntamente com dynamic_cast. Outra variantes que podem ser 
inspiradas no enum ou traits do rust etc.. Muitas das coisas começam a ser mais 
interessantes com a ajuda do compilador.
Com C++ dá para fazer algo com templates, mas acredito que o resultado final 
(até em termos de diagnósticos) é melhor com suporte do compilador.

Minha ideia é implementar isso e outras coisas e criar o meu C "+ ou -" gerando código C.





 

Rodrigo Madera

unread,
Apr 26, 2017, 11:12:13 PM4/26/17
to ccppb...@googlegroups.com

From: ccppb...@googlegroups.com <ccppb...@googlegroups.com> on behalf of Thiago Adams <thiago...@gmail.com>
Sent: Tuesday, April 25, 2017 9:16:22 PM
To: ccppbrasil
Subject: [ccppbrasil] Facilitador para implementar um tipo de polimorfismo em C
 
--
Antes de enviar um e-mail para o grupo leia:
http://www.ccppbrasil.org/wiki/Lista:AntesdePerguntar
--~--~---------~--~----~------------------------------
[&] C & C++ Brasil - http://www.ccppbrasil.org/
Para sair dessa lista, envie um e-mail para ccppbrasil-...@googlegroups.com

Thiago Adams

unread,
Apr 27, 2017, 8:33:25 AM4/27/17
to ccppbrasil


On Thursday, April 27, 2017 at 12:12:13 AM UTC-3, Rodrigo Madera wrote:

Complemento:
 

Ivan lopes

unread,
Apr 27, 2017, 2:03:49 PM4/27/17
to ccppb...@googlegroups.com
eu gostei muito do gerador ... costumo escrever geradores em python ... com python cheetah ... 
para gerar códigos C/C++ ... 

parabéns! 

--
Antes de enviar um e-mail para o grupo leia:
http://www.ccppbrasil.org/wiki/Lista:AntesdePerguntar
--~--~---------~--~----~------------------------------
[&] C & C++ Brasil - http://www.ccppbrasil.org/
Para sair dessa lista, envie um e-mail para ccppbrasil-unsubscribe@googlegroups.com

Thiago Adams

unread,
Oct 6, 2017, 4:41:59 PM10/6/17
to ccppbrasil


On Tuesday, April 25, 2017 at 9:16:23 PM UTC-3, Thiago Adams wrote:

Fiz um gerador de "casts" e funcoes "virtuais"para criar um tipo de polimorfismo em C.
O tipo de polimorfismo usa um id intrusivo por instancia.
Existem ponteiros para os tipos que pode apontar para diferentes tipos concretos.
A pagina carrega um exemplo simples.


O trabalho do gerador eh explodir os tipos e achar as instancias criando os possiveis  casts e tambem 
um exemplo de chamada de funcao polimorfica.
Estes casts sao defines que alem de fazer o cast deixam o fonte mais seguro por nao usar nenhum cast foçado.
os defines guiam o programador para os casts permitidos.




Fiz um experimento com uma implementação alternativa com unions.
A diferença é que um tipo de cast, exemplo shape -> box pode ser feito naturalmente
com o "auto-complete" e IDE atuais.
Só não consegui fazer um warning no inverso "box as shape".


typedef struct Box
{
    int id;
    int w;
    int h;
} Box;

typedef struct Circle
{
    int id;
    int r;
} Circle;
#define CIRCLE_ID 123

Circle* Circle_Create()
{
    struct Circle *p = malloc(sizeof * p);
    if (p)
    {
        p->id = CIRCLE_ID;
        p->r = 0;
    }
    return p;
}

void Circle_Delete(struct Circle* pCircle)
{
    if (pCircle)
    {
        free(pCircle);
    }

}

typedef struct Other
{
    int id;
    int r;
} Other;

typedef union 
{
    int id;
    Box AsBox;
    Circle AsCircle;
} Shape;

typedef union Serializable
{
    int id;
    Other AsOther;
    Shape AsShape;
} Serializable;

void Shape_Draw(Shape* p)
{
    switch (p->id)
    {
    case CIRCLE_ID:
        printf("radius = %d", p->AsCircle.r);
    default:
        break;
    }
}


int main()
{
    Circle* pCircle = Circle_Create();
    Shape* pShape = (Shape*) pCircle;
    Serializable* pSerializable = (Serializable*)pShape;

    if (pSerializable->id == CIRCLE_ID)
    {
        printf("%d", pSerializable->AsShape.AsCircle.r);
    }

    
    Shape_Draw(pShape);
    
    Circle_Delete(&pShape->AsCircle);
    return 0;
}


Virgilio Fornazin

unread,
Oct 11, 2017, 8:41:44 AM10/11/17
to ccppb...@googlegroups.com
No objetivo final, qualquer semelhança com o original CFront é mera coincidencia...

Thiago Adams

unread,
Oct 11, 2017, 1:11:26 PM10/11/17
to ccppbrasil


On Wednesday, October 11, 2017 at 9:41:44 AM UTC-3, Virgilio Fornazin wrote:
No objetivo final, qualquer semelhança com o original CFront é mera coincidencia...


Considerando que estamos comparando o C' com 
CFront, Cfront usava virtuais e ele gerava código C (expandido - sem macros) que não era para ser mantido pelo 
programador.
Existe uma certa semelhança C' e typescript caso for usar o C' para gerar código separado.
Seria possível ler C11 e exportar C89  por exemplo.

Mas o foco agora esta mais para 'pair programming' você e o C'robô trabalhando no mesmo código.


Virgilio Fornazin

unread,
Oct 11, 2017, 1:45:45 PM10/11/17
to ccppb...@googlegroups.com
sim... disse que é semelhante.


Thiago Adams

unread,
Mar 1, 2023, 9:03:40 AM3/1/23
to ccppbrasil
Reply all
Reply to author
Forward
0 new messages