web-dev-qa-db-fra.com

Les structures C opaques: comment doivent-elles être déclarées?

J'ai vu les deux styles suivants de déclaration de types opaques dans les API C. Y at-il un avantage évident à utiliser un style par rapport à l’autre?

Option 1

// foo.h
typedef struct foo * fooRef;
void doStuff(fooRef f);

// foo.c
struct foo {
    int x;
    int y;
};

Option 2

// foo.h
typedef struct _foo foo;
void doStuff(foo *f);

// foo.c
struct _foo {
    int x;
    int y;
};
40
splicer

Mon vote est pour la troisième option que mouviciel a posté puis supprimée:

J'ai vu une troisième voie:

// foo.h
struct foo;
void doStuff(struct foo *f);

// foo.c
struct foo {
    int x;
    int y;
};

Si vous ne supportez vraiment pas la saisie du mot clé struct, typedef struct foo foo; (remarque: éliminez le trait de soulignement inutile et problématique) est acceptable. Mais quoi que vous fassiez, jamais utilisez typedef pour définir les noms des types de pointeur. Cela cache l’information extrêmement importante que les variables de ce type référencent un objet qui peut être modifié chaque fois que vous les transmettez à des fonctions, et cela rend le traitement de versions du qualifié différentes (par exemple, const- qualifié) du pointeur .

65
R..

bar(const fooRef) déclare une adresse immuable en argument. bar(const foo *) déclare l'adresse d'un foo immuable en tant qu'argument.

Pour cette raison, j'ai tendance à préférer l'option 2. En d'autres termes, le type d'interface présenté est celui dans lequel cv-ness peut être spécifié à chaque niveau d'indirection. Bien sûr, un can évite l’option 1 du rédacteur de la bibliothèque et n’utilise que foo, en vous ouvrant à toutes sortes d’horreurs lorsque le rédacteur de la bibliothèque modifie l’implémentation. (C'est-à-dire que l'auteur de l'option 1 perçoit uniquement que fooRef fait partie de l'interface invariante et que foo peut venir, aller, être modifié, peu importe. L'émetteur de l'option 2 perçoit que foo fait partie de l'interface invariante.

Je suis plus surpris que personne n'ait suggéré de construire une combinaison typedef/struct.
typedef struct { ... } foo;

1
Eric Towers

Option 1.5

Je suis habitué à utiliser Option 1 , sauf si vous appelez votre référence avec _h pour indiquer qu'il s'agit d'un "descripteur" d'un "objet" de style C de cette "classe" C. Ensuite, vous vous assurez que vos prototypes de fonctions utilisent const lorsque le contenu de cet objet "handle" est uniquement une entrée et ne peut pas être modifié. N'utilisez pas const où le contenu peut être modifié.

Voici un exemple complet:

//======================================================================================================================
// my_module.h
//======================================================================================================================

// An opaque pointer (handle) to a C-style "object" of "class" type "my_module" (struct my_module_s *, or my_module_h):
typedef struct my_module_s *my_module_h;

// Create a new "object" of "class" "my_module":
// A function that takes a *pointer to* an "object" handle, `malloc`s memory for a new copy of the opaque 
// `struct my_module_s`, then points the user's input handle (via its passed-in pointer) to this newly-created 
// "object" of "class" "my_module".
void my_module_open(my_module_h * my_module_h_p);

// A function that takes this "object" (via its handle) as an input only and cannot modify it
void my_module_do_stuff1(const my_module_h my_module);

// A function that can modify the private content of this "object" (via its handle) (but still cannot modify the 
// handle itself)
void my_module_do_stuff2(my_module_h my_module);

// Destroy the passed-in "object" of "class" type "my_module":
// A function that can close this object by stopping all operations, as required, and `free`ing its memory.
// `struct my_module_s`, then points the user's input handle (via its passed-in pointer) to this newly-created "object".
void my_module_close(my_module_h my_module);

//======================================================================================================================
// my_module.c
//======================================================================================================================

// Definition of the opaque struct "object" of C-style "class" "my_module".
// - NB: Since this is an opaque struct (declared in the header but not defined until the source file), it has the 
// following 2 important properties:
// 1) It permits data hiding, wherein you end up with the equivalent of a C++ "class" with only *private* member 
// variables.
// 2) Objects of this "class" can only be dynamically allocated. No static allocation is possible since any module
// including the header file does not know the contents of *nor the size of* (this is the critical part) this "class"
// (ie: C struct).
struct my_module_s
{
    int my_private_int1;
    int my_private_int2;
    float my_private_float;
    // etc. etc--add more "private" member variables as you see fit
}

void my_module_open(my_module_h * my_module_h_p)
{
    // Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault to try to dereference 
    // a NULL pointer)
    if (!my_module_h_p)
    {
        // Print some error or store some error code here, and return it at the end of the function instead of 
        // returning void.
        goto done;
    }

    // Now allocate the actual memory for a new my_module C object from the heap, thereby dynamically creating this
    // C-style "object".
    my_module_h my_module; // Create a local object handle (pointer to a struct)
    my_module = malloc(sizeof(*my_module)); // Dynamically allocate memory for the full contents of the struct "object"
    if (!my_module) 
    {
        // Malloc failed due to out-of-memory. Print some error or store some error code here, and return it
        // at the end of the function instead of returning void.
        goto done;
    }

    // Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!)
    memset(my_module, 0, sizeof(*my_module));

    // Now pass out this object to the user, and exit.
    *my_module_h_p = my_module;

done:
}

void my_module_do_stuff1(const my_module_h my_module)
{
    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    {
        goto done;
    }

    // Do stuff where you use my_module private "member" variables.
    // Ex: use `my_module->my_private_int1` here, or `my_module->my_private_float`, etc. 

done:
}

void my_module_do_stuff2(my_module_h my_module)
{
    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    {
        goto done;
    }

    // Do stuff where you use AND UPDATE my_module private "member" variables.
    // Ex:
    my_module->my_private_int1 = 7;
    my_module->my_private_float = 3.14159;
    // Etc.

done:
}

void my_module_close(my_module_h my_module)
{
    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    {
        goto done;
    }

    free(my_module);

done:
}

Les seules améliorations au-delà seraient:

  1. Implémentez la gestion complète des erreurs et renvoyez l'erreur au lieu de void.
  2. Ajoutez une structure de configuration appelée my_module_config_t au fichier .h et transmettez-la à la fonction open pour mettre à jour les variables internes lorsque vous créez un nouvel objet. Exemple:

    //--------------------
    // my_module.h
    //--------------------
    
    // my_module configuration struct
    typedef struct my_module_config_s
    {
        int my_config_param_int;
        int my_config_param_float;
    } my_module_config_t;
    
    void my_module_open(my_module_h * my_module_h_p, const my_module_config_t *config);
    
    //--------------------
    // my_module.c
    //--------------------
    
    void my_module_open(my_module_h * my_module_h_p, const my_module_config_t *config)
    {
        // Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault to try to dereference 
        // a NULL pointer)
        if (!my_module_h_p)
        {
            // Print some error or store some error code here, and return it at the end of the function instead of 
            // returning void.
            goto done;
        }
    
        // Now allocate the actual memory for a new my_module C object from the heap, thereby dynamically creating this
        // C-style "object".
        my_module_h my_module; // Create a local object handle (pointer to a struct)
        my_module = malloc(sizeof(*my_module)); // Dynamically allocate memory for the full contents of the struct "object"
        if (!my_module) 
        {
            // Malloc failed due to out-of-memory. Print some error or store some error code here, and return it
            // at the end of the function instead of returning void.
            goto done;
        }
    
        // Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!)
        memset(my_module, 0, sizeof(*my_module));
    
        // Now initialize the object with values per the config struct passed in.
        my_module->my_private_int1 = config->my_config_param_int;
        my_module->my_private_int2 = config->my_config_param_int*3/2;
        my_module->my_private_float = config->my_config_param_float;        
        // etc etc
    
        // Now pass out this object to the user, and exit.
        *my_module_h_p = my_module;
    
    done:
    }
    
0
Gabriel Staples