web-dev-qa-db-fra.com

Des structures de données génériques adaptées aux types dans un C vieux?

J'ai fait beaucoup plus de programmation C++ que de programmation "classique". Une chose qui me manque énormément lorsque la programmation en C clair est la structure de données générique, qui est fournie en C++ et qui est fournie en C++ via des modèles.

Par souci de concret, considérons une liste générique à liens simples. En C++, il est simple de définir votre propre classe de modèle, puis de l’instancier pour les types dont vous avez besoin.

En C, je peux penser à quelques façons de mettre en oeuvre une liste générique à lien unique:

  1. Ecrivez une fois le (s) type (s) de liste lié (s) et les procédures associées en utilisant des pointeurs vides pour faire le tour du système de types.
  2. Ecrivez des macros de préprocesseur en prenant les noms de type nécessaires, etc., pour générer une version spécifique à un type de la structure de données et des procédures associées.
  3. Utilisez un outil autonome plus sophistiqué pour générer le code correspondant aux types dont vous avez besoin.

Je n'aime pas l'option 1, car elle subvertit le système de types et aurait probablement de moins bonnes performances qu'une implémentation spécialisée spécifique à un type. À mon avis, l'utilisation d'une représentation uniforme de la structure de données pour tous les types et la transposition vers/depuis des pointeurs vides nécessitent une indirection qui serait évitée par une implémentation spécialisée pour le type d'élément.

L'option 2 ne nécessite aucun outil supplémentaire, mais elle est quelque peu maladroite et peut générer des erreurs de compilation en cas d'utilisation incorrecte.

L'option 3 pourrait donner de meilleurs messages d'erreur au compilateur que l'option 2, car le code de la structure de données spécialisée résiderait sous une forme développée pouvant être ouverte dans un éditeur et inspectée par le programmeur (par opposition au code généré par les macros du préprocesseur). Cependant, cette option est le plus lourd des poids, une sorte de "modèles du pauvre". J'ai déjà utilisé cette approche auparavant, en utilisant un script sed simple pour spécialiser une version "basée sur un modèle" de code C.

J'aimerais programmer mes futurs projets "de bas niveau" en C plutôt qu'en C++, mais j'ai eu peur de réécrire des structures de données communes pour chaque type spécifique.

Quelle expérience les gens ont-ils avec ce problème? Existe-t-il de bonnes bibliothèques de structures de données génériques et d’algorithmes en C qui ne soient pas compatibles avec l’option 1 (c’est-à-dire qui utilisent des pointeurs vides, ce qui sacrifie la sécurité de type et ajoute un niveau d’indirection)?

49
Bradford Larsen

L'option 1 est l'approche adoptée par la plupart des implémentations C de conteneurs génériques que je vois. Le kit de pilotes Windows et le noyau Linux utilisent une macro pour permettre aux liens des conteneurs d'être intégrés n'importe où dans une structure, la macro étant utilisée pour obtenir le pointeur de structure à partir d'un pointeur sur le champ de lien:

L'option 2 est la solution retenue par les implémentations de conteneur tree.h et queue.h de BSD:

Je ne pense pas que l'une ou l'autre de ces approches serait considérée comme sûre. Utile, mais pas en sécurité.

21
Michael Burr

C a une beauté différente de celle de C++, et la sécurité est de nature à pouvoir toujours voir ce que tout est lors du traçage dans le code sans impliquer de transtypages dans votre débogueur.

La beauté de C découle en grande partie de son manque de sécurité de type, de travail autour du système de type et au niveau brut des bits et des octets. À cause de cela, il peut faire plus facilement certaines choses sans lutter contre le langage, comme par exemple les structures de longueur variable, utiliser la pile même pour les tableaux dont la taille est déterminée au moment de l'exécution, etc. En outre, il a tendance à être beaucoup plus simple. préservez ABI lorsque vous travaillez à ce niveau inférieur.

Il y a donc une esthétique différente et des défis différents, et je recommanderais un changement de mentalité lorsque vous travaillez en C. Pour l'apprécier vraiment, je suggérerais de faire des choses que beaucoup de gens prennent pour acquis ces jours-ci, comme mettre en œuvre votre propre allocateur de mémoire ou pilote de périphérique. Lorsque vous travaillez à un niveau aussi bas, vous ne pouvez pas vous empêcher de tout considérer comme des dispositions de mémoire en bits et en octets, par opposition à des «objets» auxquels sont associés des comportements. En outre, il peut arriver un point dans ce code de manipulation de bits/octets de bas niveau où C devient plus facile à comprendre que le code C++ jonché de reinterpret_casts, par ex.

En ce qui concerne votre exemple de liste chaînée, je suggérerais une version non intrusive d’un nœud lié (qui ne nécessite pas de stocker des pointeurs de liste dans le type d’élément, T, elle-même, ce qui permet de découpler la logique et la représentation de la liste chaînée de T. ), ainsi:

struct ListNode
{
    struct ListNode* prev;
    struct ListNode* next;
    MAX_ALIGN char element[1]; // Watch out for alignment here.
                               // see your compiler's specific info on 
                               // aligning data members.
};

Nous pouvons maintenant créer un noeud de liste comme ceci:

struct ListNode* list_new_node(int element_size)
{
    // Watch out for alignment here.
    return malloc_max_aligned(sizeof(struct ListNode) + element_size - 1);
}

// create a list node for 'struct Foo'
void foo_init(struct Foo*);
struct ListNode* foo_node = list_new_node(sizeof(struct Foo));
foo_init(foo_node->element);

Pour récupérer l'élément de la liste en tant que T *:

T* element = list_node->element;

Comme il s’agit du C, il n’ya aucune vérification typographique lorsqu’on lance des pointeurs de cette manière, et cela vous donnera probablement aussi un sentiment de malaise si vous venez d’un fond C++.

La partie délicate à utiliser ici est de s’assurer que ce membre, element, est correctement aligné pour le type que vous souhaitez stocker. Lorsque vous pouvez résoudre ce problème aussi facilement que vous le souhaitez, vous disposez d'une solution puissante pour créer des dispositions de mémoire et des allocateurs efficaces. Souvent, cela vous obligera simplement à utiliser l'alignement maximum pour tout ce qui peut sembler inutile, mais ce n'est généralement pas le cas si vous utilisez des structures de données et des allocateurs appropriés qui ne paient pas cette surcharge pour de nombreux petits éléments sur une base individuelle.

Maintenant, cette solution implique toujours le transtypage. Vous pouvez faire peu de choses à ce sujet sans avoir une version distincte du code de ce nœud de liste et la logique correspondante pour l'utiliser avec chaque type, T, que vous souhaitez prendre en charge (à l'exception du polymorphisme dynamique). Cependant, cela n'implique pas un niveau supplémentaire d'indirection, comme vous auriez pu le penser nécessaire, et alloue tout le nœud de liste et l'élément en une seule allocation.

Et je recommanderais ce moyen simple de réaliser la généricité en C dans de nombreux cas. Remplacez simplement T par un tampon dont la longueur correspond à sizeof(T) et alignée correctement. Si vous disposez d'un moyen raisonnablement portable et sûr que vous pouvez généraliser pour assurer un alignement correct, vous disposerez d'un moyen très puissant de travailler avec la mémoire d'une manière qui améliore souvent les accès au cache, réduit la fréquence des allocations/désallocations de segments de mémoire, indirection requise, temps de construction, etc.

Si vous avez besoin de plus d'automatisation, comme list_new_node, initialisez automatiquement struct Foo, je vous recommanderais de créer une structure de table de types générale contenant des informations telles que la taille de T, un pointeur de fonction pointant sur une fonction pour créer une instance par défaut de T, un autre pour copier T, cloner T, détruire T, un comparateur, etc. En C++, vous pouvez générer ce tableau automatiquement à l'aide de modèles et de concepts de langage intégrés tels que les constructeurs de copie et les destructeurs. C nécessite un peu plus de travail manuel, mais vous pouvez toujours le réduire un peu avec les macros.

Une autre astuce qui peut être utile si vous choisissez une route de génération de code plus macro-orientée est d'encaisser une convention de nommage d'identifiants basée sur un préfixe ou un suffixe. Par exemple, CLONE (Type, ptr) peut être défini pour renvoyer Type##Clone(ptr), de sorte que CLONE(Foo, foo) puisse appeler FooClone(foo). C'est une sorte de triche pour obtenir quelque chose qui ressemble à une surcharge de fonction en C, et est utile pour générer du code en bloc (lorsque CLONE est utilisé pour implémenter une autre macro) ou même un peu de copier/coller du code de type passe-partout au moins améliorer l'uniformité du passe-partout.

19
Dragon Energy

La plupart des programmes C utilisent l’option 1, soit en utilisant void *, soit une variante basée sur union, et elle peut vous offrir de MEILLEURES performances que le style C++/macro avec plusieurs implémentations pour différents types, car il y a moins de duplication de code, et donc moins de cache. pression et moins d'icache manqué.

3
Chris Dodd

GLib contient un ensemble de structures de données génériques, http://www.gtk.org/

CCAN a un tas d'extraits utiles et tels http://ccan.ozlabs.org/

2
Spudd86

J'utilise des pointeurs void (void *) pour représenter des structures de données génériques définies avec structs et typedefs. Ci-dessous, je partage mon implémentation d'une lib sur laquelle je travaille.

Avec ce type d'implémentation, vous pouvez penser à chaque nouveau type, défini avec typedef, comme une pseudo-classe. Ici, cette pseudo-classe est l'ensemble du code source (some_type_implementation.c) et de son fichier d'en-tête (some_type_implementation.h). 

Dans le code source, vous devez définir la structure qui présentera le nouveau type. Notez la structure dans le fichier source "node.c". Là, j'ai créé un pointeur vide sur l'attribut "info". Ce pointeur peut contenir n’importe quel type de pointeur (je pense), mais le prix que vous devez payer est un identificateur de type à l’intérieur de la structure (int type) et tous les commutateurs permettant de définir le propper handle de chaque type. Donc, dans le fichier d'en-tête node.h ", j'ai défini le type" Node "(juste pour éviter de devoir taper struct node à chaque fois), et j'ai également dû définir les constantes" EMPTY_NODE "," COMPLEX_NODE "et" MATRIX_NODE ".

Vous pouvez effectuer la compilation à la main avec "gcc * .c -lm".

main.c Fichier source

#include <stdio.h>
#include <math.h>

#define PI M_PI

#include "complex.h"
#include "matrix.h"
#include "node.h" 


int main()
{
    //testCpx();
    //testMtx();
    testNode();

    return 0;
}

node.c Fichier source

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "node.h"
#include "complex.h"
#include "matrix.h"

#define PI M_PI


struct node
{
    int type;

    void* info;
};


Node* newNode(int type,void* info)
{
    Node* newNode = (Node*) malloc(sizeof(Node));

    newNode->type = type;

    if(info != NULL)
    {
        switch(type)
        {
            case COMPLEX_NODE:
                newNode->info = (Complex*) info;
            break;

            case MATRIX_NODE:
                newNode->info = (Matrix*) info;
            break;
        }
    }
    else
        newNode->info = NULL;

    return newNode;
}

int emptyInfoNode(Node* node)
{
    return (node->info == NULL);
}

void printNode(Node* node)
{
    if(emptyInfoNode(node))
    {
        printf("Type:%d\n",node->type);
        printf("Empty info\n");
    }
    else
    {
        switch(node->type)
        {
            case COMPLEX_NODE:
                printCpx(node->info);
            break;

            case MATRIX_NODE:
                printMtx(node->info);
            break;
        }
    }
}

void testNode()
{
    Node *node1,*node2, *node3;
    Complex *Z;
    Matrix *M;

    Z = mkCpx(POLAR,5,3*PI/4);

    M = newMtx(3,4,PI);

    node1 = newNode(COMPLEX_NODE,Z);
    node2 = newNode(MATRIX_NODE,M);
    node3 = newNode(EMPTY_NODE,NULL);



    printNode(node1);
    printNode(node2);
    printNode(node3);
}

fichier d'en-tête node.h

#define EMPTY_NODE   0
#define COMPLEX_NODE 1
#define MATRIX_NODE  2


typedef struct node Node;


Node* newNode(int type,void* info);
int emptyInfoNode(Node* node);
void printNode(Node* node);
void testNode();

matrix.c Fichier source

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "matrix.h"

struct matrix
{
    // Meta-information about the matrix 
    int rows;
    int cols;

    // The elements of the matrix, in the form of a vector 
    double** MTX;
};

Matrix* newMtx(int rows,int cols,double value)
{
    register int row , col;
    Matrix* M = (Matrix*)malloc(sizeof(Matrix));

    M->rows = rows;
    M->cols = cols;
    M->MTX = (double**) malloc(rows*sizeof(double*));

    for(row = 0; row < rows ; row++)
    {
        M->MTX[row] = (double*) malloc(cols*sizeof(double));

        for(col = 0; col < cols ; col++) 
            M->MTX[row][col] = value;
    }

    return M;
}

Matrix* mkMtx(int rows,int cols,double** MTX)
{   
    Matrix* M;
    if(MTX == NULL)
    {
        M = newMtx(rows,cols,0);
    }
    else
    {
        M = (Matrix*)malloc(sizeof(Matrix));
        M->rows = rows;
        M->cols = cols;
        M->MTX  = MTX;
    }
    return M;
}

double getElemMtx(Matrix* M , int row , int col)
{
    return M->MTX[row][col];
}

void printRowMtx(double* row,int cols)
{
    register int j;
    for(j = 0 ; j < cols ; j++) 
        printf("%g ",row[j]);           
}

void printMtx(Matrix* M)
{
    register int row = 0, col = 0;

    printf("\vSize\n");
    printf("\tRows:%d\n",M->rows);
    printf("\tCols:%d\n",M->cols);
    printf("\n");
    for(; row < M->rows ; row++)
    {
        printRowMtx(M->MTX[row],M->cols);
        printf("\n");
    }

    printf("\n");
}

void testMtx()
{
    Matrix* M = mkMtx(10,10,NULL);
    printMtx(M);
}

matrix.h Fichier d'en-tête

typedef struct matrix Matrix;

Matrix* newMtx(int rows,int cols,double value);
Matrix* mkMatrix(int rows,int cols,double** MTX);
void print(Matrix* M);
double getMtx(Matrix* M , int row , int col);
void printRowMtx(double* row,int cols);
void printMtx(Matrix* M);
void testMtx();

complex.c Fichier source

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "complex.h"

struct complex
{
    int type;

    double a;
    double b;
};

Complex* mkCpx(int type,double a,double b)
{
    /** Doc - {{{
     * This function makes a new Complex number.
     * 
     * @params:
     * |-->type: Is an interger that denotes if the number is in
     * |         the analitic or in the polar form.
     * |         ANALITIC:0
     * |         POLAR   :1
     * |
     * |-->a: Is the real part if type = 0 and is the radius if 
     * |      type = 1
     * |
     * `-->b: Is the imaginary part if type = 0 and is the argument
     *        if type = 1
     * 
     * @return:
     *      Returns the new Complex number initialized with the values 
     *      passed
     *}}} */

    Complex* number = (Complex*)malloc(sizeof(Complex));

    number->type = type;
    number->a    = a;
    number->b    = b;

    return number;
}

void printCpx(Complex* number)
{
    switch(number->type)
    {
        case ANALITIC:
            printf("Re:%g | Im:%g\n",number->a,number->b);
        break;

        case POLAR:
            printf("Radius:%g | Arg:%g\n",number->a,number->b);
        break;
    }
}

void testCpx()
{
    Complex* Z = mkCpx(ANALITIC,3,2);
    printCpx(Z);
}

complex.h Fichier d'en-tête

#define ANALITIC 0 
#define POLAR    1 

typedef struct complex Complex;

Complex* mkCpx(int type,double a,double b);
void printCpx(Complex* number);
void testCpx();

J'espère que je n'ai rien manqué.

1
user4713908

Il existe une variante commune à l'option 1 qui est plus efficace car elle utilise des unions pour stocker les valeurs dans les nœuds de liste, c'est-à-dire qu'il n'y a pas d'indirection supplémentaire. Cela a l'inconvénient que la liste n'accepte que les valeurs de certains types et gaspille éventuellement de la mémoire si les types sont de tailles différentes.

Cependant, il est possible de se débarrasser de la union en utilisant plutôt un membre de groupe flexible si vous êtes prêt à rompre le crénelage strict. Exemple de code C99:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct ll_node
{
    struct ll_node *next;
    long long data[]; // use `long long` for alignment
};

extern struct ll_node *ll_unshift(
    struct ll_node *head, size_t size, void *value);

extern void *ll_get(struct ll_node *head, size_t index);

#define ll_unshift_value(LIST, TYPE, ...) \
    ll_unshift((LIST), sizeof (TYPE), &(TYPE){ __VA_ARGS__ })

#define ll_get_value(LIST, INDEX, TYPE) \
    (*(TYPE *)ll_get((LIST), (INDEX)))

struct ll_node *ll_unshift(struct ll_node *head, size_t size, void *value)
{
    struct ll_node *node = malloc(sizeof *node + size);
    if(!node) assert(!"PANIC");

    memcpy(node->data, value, size);
    node->next = head;

    return node;
}

void *ll_get(struct ll_node *head, size_t index)
{
    struct ll_node *current = head;
    while(current && index--)
        current = current->next;
    return current ? current->data : NULL;
}

int main(void)
{
    struct ll_node *head = NULL;
    head = ll_unshift_value(head, int, 1);
    head = ll_unshift_value(head, int, 2);
    head = ll_unshift_value(head, int, 3);

    printf("%i\n", ll_get_value(head, 0, int));
    printf("%i\n", ll_get_value(head, 1, int));
    printf("%i\n", ll_get_value(head, 2, int));

    return 0;
}
1
Christoph

Votre option 1 est ce que la plupart des programmeurs de l'ancien temps choisiraient, éventuellement salée avec un peu de 2 pour réduire le typage répétitif, et juste peut-être employant quelques indicateurs de fonction pour une touche de polymorphisme.

1
dmckee

Une vieille question, je le sais, mais au cas où elle serait toujours intéressante: j’essayais aujourd’hui d’utiliser l’option 2) (macros de pré-processeur) et j’ai donné l’exemple que je vais coller ci-dessous. Légèrement maladroit, mais pas terrible. Le code n'est pas totalement sûr, mais contient des contrôles de cohérence pour assurer un niveau de sécurité raisonnable. Et le traitement des messages d'erreur du compilateur lors de l'écriture était léger comparé à ce que j'ai vu lorsque les modèles C++ sont entrés en jeu. Il est probablement préférable de commencer à lire ceci à l’exemple. Utilisez le code dans la fonction "principale".

#include <stdio.h>

#define LIST_ELEMENT(type) \
    struct \
    { \
        void *pvNext; \
        type value; \
    }

#define ASSERT_POINTER_TO_LIST_ELEMENT(type, pElement) \
    do { \
        (void)(&(pElement)->value  == (type *)&(pElement)->value); \
        (void)(sizeof(*(pElement)) == sizeof(LIST_ELEMENT(type))); \
    } while(0)

#define SET_POINTER_TO_LIST_ELEMENT(type, pDest, pSource) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pSource); \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        void **pvDest = (void **)&(pDest); \
        *pvDest = ((void *)(pSource)); \
    } while(0)

#define LINK_LIST_ELEMENT(type, pDest, pSource) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pSource); \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        (pDest)->pvNext = ((void *)(pSource)); \
    } while(0)

#define TERMINATE_LIST_AT_ELEMENT(type, pDest) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        (pDest)->pvNext = NULL; \
    } while(0)

#define ADVANCE_POINTER_TO_LIST_ELEMENT(type, pElement) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pElement); \
        void **pvElement = (void **)&(pElement); \
        *pvElement = (pElement)->pvNext; \
    } while(0)

typedef struct { int a; int b; } mytype;

int main(int argc, char **argv)
{
    LIST_ELEMENT(mytype) el1;
    LIST_ELEMENT(mytype) el2;
    LIST_ELEMENT(mytype) *pEl;
    el1.value.a = 1;
    el1.value.b = 2;
    el2.value.a = 3;
    el2.value.b = 4;
    LINK_LIST_ELEMENT(mytype, &el1, &el2);
    TERMINATE_LIST_AT_ELEMENT(mytype, &el2);
    printf("Testing.\n");
    SET_POINTER_TO_LIST_ELEMENT(mytype, pEl, &el1);
    if (pEl->value.a != 1)
        printf("pEl->value.a != 1: %d.\n", pEl->value.a);
    ADVANCE_POINTER_TO_LIST_ELEMENT(mytype, pEl);
    if (pEl->value.a != 3)
        printf("pEl->value.a != 3: %d.\n", pEl->value.a);
    ADVANCE_POINTER_TO_LIST_ELEMENT(mytype, pEl);
    if (pEl != NULL)
        printf("pEl != NULL.\n");
    printf("Done.\n");
    return 0;
}
1
michaeljt

J'aimerais programmer mes futurs projets "de bas niveau" en C plutôt qu'en C++ ...

Pourquoi? Votre cible manque-t-elle d'un compilateur C++ ou d'un runtime C++? 

0
John

J'utilise l'option 2 pour quelques collections hautes performances, et cela prend énormément de temps de travailler sur la quantité de macro-logique nécessaire pour faire quelque chose de vraiment générique au moment de la compilation et qui en vaille la peine. Je le fais uniquement pour la performance brute (jeux). Une approche X-macros est utilisée.

L’option 2 est sans cesse un problème douloureux: «En supposant un nombre fini d’options, telles que des clés 8/16/32/64 bits, est-ce que je fais de cette valeur une constante et que je définis plusieurs fonctions, chacune avec un élément différent de celui-ci? ensemble de valeurs que constante peut prendre, ou dois-je simplement en faire une variable membre? " Le premier signifie un cache d’instruction moins performant puisque vous avez beaucoup de fonctions répétées avec seulement un ou deux nombres différents, tandis que le dernier signifie que vous devez référencer des variables allouées, ce qui dans le pire des cas signifie un cache de données manquant. Puisque l’option 1 est purement dynamique, vous ferez de ces valeurs des variables membres sans même y penser. C'est vraiment de la micro-optimisation, cependant.

N'oubliez pas non plus le compromis entre les pointeurs renvoyés et les valeurs: cette dernière est la plus performante lorsque la taille de l'élément de données est inférieure ou égale à la taille du pointeur; par contre, si l'élément de données est plus volumineux, il est probablement préférable de renvoyer des pointeurs que de forcer la copie d'un objet volumineux en renvoyant une valeur.

Je suggérerais fortement de choisir l'option 1 dans tous les scénarios où vous n'êtes pas sûr à 100% que les performances de collecte constitueront votre goulot d'étranglement. Même avec mon utilisation de l’Option 2, ma bibliothèque de collections fournit une "configuration rapide" qui ressemble à l’Option 1, c’est-à-dire l’utilisation des valeurs void * dans ma liste et ma carte. Cela suffit pour 90% ou plus des circonstances.

0
Engineer