web-dev-qa-db-fra.com

Fonctions statiques déclarées dans les fichiers d'en-tête "C"

Pour moi, c'est une règle pour définir et déclarer des fonctions statiques dans les fichiers source, je veux dire les fichiers .c.

Cependant, dans de très rares situations, j'ai vu des gens le déclarer dans le fichier d'en-tête. Étant donné que les fonctions statiques ont un lien interne, nous devons le définir dans chaque fichier, nous incluons le fichier d'en-tête où la fonction est déclarée. Cela semble assez étrange et loin de ce que nous voulons habituellement lorsque nous déclarons quelque chose de statique.

D'un autre côté, si quelqu'un de naïf essaie d'utiliser cette fonction sans la définir, le compilateur se plaindra. Donc, dans un certain sens, il n'est pas vraiment dangereux de faire cela, même si cela semble étrange.

Mes questions sont:

  • Quel est le problème de la déclaration des fonctions statiques dans les fichiers d'en-tête?
  • Quels sont les risques?
  • Quel impact en temps de compilation?
  • Existe-t-il un risque lors de l'exécution?
18
miguel azevedo

J'aimerais d'abord clarifier ma compréhension de la situation que vous décrivez: l'en-tête contient (uniquement) une déclaration de fonction statique tandis que le fichier C contient la définition, c'est-à-dire le code source de la fonction. Par exemple

some.h:

static void f();
// potentially more declarations

some.c:

#include "some.h"
static void f() { printf("Hello world\n"); }
// more code, some of it potentially using f()

Si telle est la situation que vous décrivez, je conteste votre remarque

Étant donné que les fonctions statiques ont un lien interne, nous devons le définir dans chaque fichier, nous incluons le fichier d'en-tête où la fonction est déclarée.

Si vous déclarez la fonction mais ne l'utilisez pas dans une unité de traduction donnée, je ne pense pas que vous ayez à la définir. gcc accepte cela avec un avertissement; la norme ne semble pas l'interdire, sauf si j'ai raté quelque chose. Cela peut être important dans votre scénario car les unités de traduction qui n'utilisent pas la fonction mais incluent l'en-tête avec sa déclaration n'ont pas à fournir de définition inutilisée.


  • Quel est le problème de la déclaration de fonctions statiques dans les fichiers d'en-tête?
    C'est quelque peu inhabituel. En règle générale, les fonctions statiques sont des fonctions nécessaires dans un seul fichier. Ils sont déclarés statiques pour rendre cela explicite en limitant leur visibilité. Les déclarer dans un en-tête est donc quelque peu antithétique. Si la fonction est effectivement utilisée dans plusieurs fichiers avec des définitions identiques, elle doit être rendue externe, avec une seule définition. Si une seule unité de traduction l'utilise réellement, la déclaration n'appartient pas à un en-tête.

    Un scénario possible consiste donc à assurer une signature de fonction uniforme pour différentes implémentations dans les unités de traduction respectives. L'en-tête commun conduit à une erreur de temps de compilation pour différents types de retour en C (et C++); différents types de paramètres provoqueraient une erreur de temps de compilation uniquement en C (mais pas en C++ 'en raison d'une surcharge de fonction).
  • Quels sont les risques?
    Je ne vois pas de risques dans votre scénario. (Par opposition à l'inclusion également de la fonction définition dans un en-tête qui peut violer le principe d'encapsulation.)
  • Quel est l'impact sur le temps de compilation?
    Une déclaration de fonction est petite et sa complexité est faible, donc le surcoût d'avoir des déclarations de fonction supplémentaires dans un en-tête est probablement négligeable. Mais si vous créez et incluez un en-tête supplémentaire pour la déclaration dans de nombreuses unités de traduction, la surcharge de gestion des fichiers peut être importante (c'est-à-dire que le compilateur tourne beaucoup au ralenti pendant qu'il attend les E/S d'en-tête)
  • Existe-t-il un risque lors de l'exécution?
    Je n'en vois aucun.
13
Peter A. Schneider

Ce n'est pas une réponse aux questions posées, mais nous espérons que pourquoi on pourrait implémenter une fonction static (ou static inline) Dans un fichier d'en-tête.

Personnellement, je ne peux penser qu'à deux bonnes raisons de déclarer certaines fonctions static dans un fichier d'en-tête:


  1. Si le fichier d'en-tête implémente complètement une interface qui ne devrait être visible que dans l'unité de compilation actuelle

    Ceci est extrêmement rare, mais pourrait être utile par exemple un contexte éducatif, à un moment donné au cours du développement d'un exemple de bibliothèque; ou peut-être lors de l'interfaçage avec un autre langage de programmation avec un minimum de code.

    Un développeur peut choisir de le faire si la bibliothèque ou l'implémentation inter-interface est triviale et presque ainsi, et la facilité d'utilisation (pour le développeur utilisant le fichier d'en-tête) est plus importante que la taille du code. Dans ces cas, les déclarations dans le fichier d'en-tête utilisent souvent des macros de préprocesseur, permettant au même fichier d'en-tête d'être inclus plusieurs fois, fournissant une sorte de polymorphisme brut en C.

    Voici un exemple pratique: terrain de jeu de tir sur pied pour les générateurs de nombres pseudo-aléatoires linéaires congruents. Étant donné que l'implémentation est locale à l'unité de compilation, chaque unité de compilation recevra ses propres copies du PRNG. Cet exemple montre également comment le polymorphisme brut peut être implémenté en C.

    prng32.h :

    #if defined(PRNG_NAME) && defined(PRNG_MULTIPLIER) && defined(PRNG_CONSTANT) && defined(PRNG_MODULUS)
    #define MERGE3_(a,b,c) a ## b ## c
    #define MERGE3(a,b,c) MERGE3_(a,b,c)
    #define NAME(name) MERGE3(PRNG_NAME, _, name)
    
    static uint32_t NAME(state) = 0U;
    
    static uint32_t NAME(next)(void)
    {
        NAME(state) = ((uint64_t)PRNG_MULTIPLIER * (uint64_t)NAME(state) + (uint64_t)PRNG_CONSTANT) % (uint64_t)PRNG_MODULUS;
        return NAME(state);
    }
    
    #undef NAME
    #undef MERGE3
    #endif
    
    #undef PRNG_NAME
    #undef PRNG_MULTIPLIER
    #undef PRNG_CONSTANT
    #undef PRNG_MODULUS
    

    Un exemple utilisant ce qui précède, example-prng32.h :

    #include <stdlib.h>
    #include <stdint.h>
    #include <stdio.h>
    
    #define PRNG_NAME       glibc
    #define PRNG_MULTIPLIER 1103515245UL
    #define PRNG_CONSTANT   12345UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides glibc_state and glibc_next() */
    
    #define PRNG_NAME       borland
    #define PRNG_MULTIPLIER 22695477UL
    #define PRNG_CONSTANT   1UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides borland_state and borland_next() */
    
    int main(void)
    {
        int i;
    
        glibc_state = 1U;
        printf("glibc lcg: Seed %u\n", (unsigned int)glibc_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)glibc_next());
        printf("%u\n", (unsigned int)glibc_next());
    
        borland_state = 1U;
        printf("Borland lcg: Seed %u\n", (unsigned int)borland_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)borland_next());
        printf("%u\n", (unsigned int)borland_next());
    
        return EXIT_SUCCESS;
    }
    

    La raison du marquage à la fois de la variable _state Et de la fonction _next()static est que chaque unité de compilation qui inclut le fichier d'en-tête a sa propre copie des variables et la fonctions - ici, leur propre copie du PRNG. Chacun doit être semé séparément, bien sûr; et s'il est ensemencé à la même valeur, donnera la même séquence.

    Il faut généralement éviter ces tentatives de polymorphisme en C, car cela conduit à des manigances de macro de préprocesseur compliquées, ce qui rend l'implémentation beaucoup plus difficile à comprendre, à maintenir et à modifier que nécessaire.

    Cependant, lorsque explore l'espace des paramètres de certains algorithmes - comme ici, les types de générateurs congruents linéaires 32 bits , cela nous permet d'utiliser une seule implémentation pour chaque des générateurs que nous examinons, en veillant à ce qu'il n'y ait pas de différences de mise en œuvre entre eux. Notez que même ce cas ressemble plus à un outil de développement, et non quelque chose que vous devriez voir dans une implémentation fournie aux autres.


  1. Si l'en-tête implémente de simples fonctions d'accesseur static inline

    Les macros de préprocesseur sont couramment utilisées pour simplifier le code d'accès aux types de structure complexes. Les fonctions static inline Sont similaires, sauf qu'elles fournissent également une vérification de type au moment de la compilation et peuvent faire référence à leurs paramètres plusieurs fois (avec les macros, ce qui est problématique).

    Un cas d'utilisation pratique est une interface simple pour lire des fichiers à l'aide d'E/S POSIX.1 de bas niveau (en utilisant <unistd.h> Et <fcntl.h> Au lieu de <stdio.h>). Je l'ai fait moi-même lors de la lecture de très gros fichiers texte (des dizaines de mégaoctets à gigaoctets) contenant des nombres réels (avec un flotteur/double analyseur personnalisé), comme le GNU C E/S standard n'est pas particulièrement rapide.

    Par exemple, inbuffer.h :

    #ifndef   INBUFFER_H
    #define   INBUFFER_H
    
    typedef struct {
        unsigned char  *head;       /* Next buffered byte */
        unsigned char  *tail;       /* Next byte to be buffered */
        unsigned char  *ends;       /* data + size */
        unsigned char  *data;
        size_t          size;
        int             descriptor;
        unsigned int    status;     /* Bit mask */
    } inbuffer;
    #define INBUFFER_INIT { NULL, NULL, NULL, NULL, 0, -1, 0 }
    
    int inbuffer_open(inbuffer *, const char *);
    int inbuffer_close(inbuffer *);
    
    int inbuffer_skip_slow(inbuffer *, const size_t);
    int inbuffer_getc_slow(inbuffer *);
    
    static inline int inbuffer_skip(inbuffer *ib, const size_t n)
    {
        if (ib->head + n <= ib->tail) {
            ib->head += n;
            return 0;
        } else
            return inbuffer_skip_slow(ib, n);
    }
    
    static inline int inbuffer_getc(inbuffer *ib)
    {
        if (ib->head < ib->tail)
            return *(ib->head++);
        else
            return inbuffer_getc_slow(ib);
    }
    
    #endif /* INBUFFER_H */
    

    Notez que les inbuffer_skip() et inbuffer_getc() ci-dessus ne vérifient pas si ib n'est pas NULL; cela est typique de ces fonctions. Ces fonctions d'accesseur sont supposées être "dans la voie rapide", c'est-à-dire appelées très souvent. Dans de tels cas, même la surcharge d'appel de la fonction est importante (et est évitée avec les fonctions static inline, Car elles sont dupliquées dans le code sur le site de l'appel).

    Les fonctions accessoires triviales, comme les inbuffer_skip() et inbuffer_getc() ci-dessus, peuvent également permettre au compilateur d'éviter les mouvements de registre impliqués dans les appels de fonction, car les fonctions s'attendent à ce que leurs paramètres soient situés dans des registres spécifiques ou sur la pile, tandis que les fonctions en ligne peuvent être adaptées (par rapport à l'utilisation du registre) au code entourant la fonction en ligne.

    Personnellement, je recommande d'abord d'écrire quelques programmes de test en utilisant les fonctions non intégrées et de comparer les performances et les résultats aux versions intégrées. La comparaison des résultats garantit que les versions intégrées n'ont pas de bogues (désactivée par un type est courant ici!), Et la comparaison des performances et des binaires générés (taille, au moins) vous indique si l'inlining en vaut la peine en général.

10
Nominal Animal

Pourquoi voudriez-vous une fonction à la fois globale et statique? En c, les fonctions sont globales par défaut. Vous n'utilisez des fonctions statiques que si vous souhaitez limiter l'accès à une fonction au fichier dans lequel elles sont déclarées. Vous restreignez donc activement l'accès en le déclarant statique ...

La seule exigence pour les implémentations dans le fichier d'en-tête concerne les fonctions de modèle c ++ et les fonctions membres de classe de modèle.

0
JHBonarius