Статические функции, объявленные в заголовочных файлах "C"



Для меня это правило для определения и объявления статических функций внутри исходных файлов, я имею в виду .c файлы.



Однако в очень редких ситуациях я видел людей, объявляющих его в заголовочном файле.
Поскольку статические функции имеют внутреннюю связь, мы должны определить ее в каждом файле, который мы включаем в заголовочный файл, где объявлена функция. Это выглядит довольно странно и далеко от того, что мы обычно хотим, когда объявляем что-то статичным.



С другой стороны, если кто-то наивный попытается использовать эту функцию без определения этого компилятор будет жаловаться. Так что в каком-то смысле не очень небезопасно это делать, даже звуча странно.



Мои вопросы:




  • в чем проблема объявления статических функций в заголовочных файлах?

  • каковы риски?

  • каково влияние во время компиляции?

  • существует ли какой-либо риск во время выполнения?

742   3  

3 ответов:

Сначала я хотел бы прояснить свое понимание ситуации, которую вы описываете: заголовок содержит (только) объявление статической функции, в то время как файл C содержит определение, т. е. исходный код функции. Например

Некоторые.h:

static void f();
// potentially more declarations

Некоторые.c:

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

Если это та ситуация, которую вы описываете, я не согласен с вашим замечанием

Так как статические функции имеют внутреннюю связь, мы должны определить ее в каждом файле, который мы включаем. заголовочный файл, в котором объявлена функция.

Если вы объявляете функцию, но не используете ее в данной единице перевода, я не думаю, что вам нужно ее определять. gcc принимает это с предупреждением; стандарт, похоже, не запрещает этого, если только я не пропустил что-то. Это может быть важно в вашем сценарии, потому что единицы перевода, которые не используют функцию, но включают заголовок с его объявлением, не должны предоставлять неиспользуемое определение.


Теперь давайте рассмотрим вопросы:
  • в чем проблема объявления статических функций в заголовочных файлах?
    это несколько необычно. Это имело бы смысл только в том случае, если большинство единиц перевода, которые включают заголовок с заданным объявлением функции, действительно используют эту функцию, потому что основное обоснование и преимущество статической функции-их ограниченная видимость. Они не загрязняют глобальное пространство имен (единственное, которое имеет C) и могут использоваться как "частные" методы бедняка, которые не предназначены для быть использованы широкой публикой и, следовательно, объявлены такими, что они доступны только там, где они необходимы.

    С другой стороны, на самом деле может быть полезно иметь объявление в заголовке, поскольку это гарантирует, что все локальные определения согласуются, по крайней мере, в сигнатуре функции. (Две функции с одинаковым именем, но разными типами возвращаемых значений вызвали бы ошибку времени компиляции в C (и C++); различные типы параметров вызвали бы ошибку времени компиляции только в C, поскольку она не имеет перегрузка функций.) С этой точки зрения единообразия может быть достаточно сразу же предоставить определение функции, соответствующее заголовочному файлу, если предполагается, что функция идентична в каждой единице перевода. Накладные расходы этого подхода зависят от того, действительно ли все единицы перевода, включающие заголовок, используют эту функцию.

  • каковы риски?
    я не вижу рисков в вашем сценарии. (В отличие от также включения функции определение в заголовке, которое может нарушать принцип инкапсуляции.)

  • каково влияние во время компиляции?
    объявление функции невелико, а ее сложность невелика, поэтому накладные расходы на дополнительные объявления функций в заголовке, скорее всего, незначительны. Но если вы создадите и включите дополнительный заголовок для объявления во многих единицах перевода, то затраты на обработку файлов могут быть значительными (т. е. компилятор много простаивает пока он ждет ввода-вывода заголовка).

  • есть ли риск во время выполнения?
    я ничего не вижу.

Это не ответ на поставленные вопросы, но, надеюсь, показывает , Почему можно реализовать функцию static (или static inline) в заголовочном файле.

Лично я могу назвать только две веские причины для объявления некоторых функций static в заголовочном файле:


  1. Если заголовочный файл полностью реализует интерфейс, который должен быть виден только в текущем блоке компиляции

    Это крайне редко, но может быть полезно, например, в образовательных целях. контекст, в какой-то момент во время разработки некоторой библиотеки примеров; или, возможно, при взаимодействии с другим языком программирования с минимальным кодом.

    Разработчик может сделать это, если библиотека или реализация interaface тривиальны и почти таковы, а простота использования (для разработчика, использующего файл заголовка) важнее размера кода. В этих случаях объявления в заголовочном файле часто используют макросы препроцессора, что позволяет включать один и тот же заголовочный файл более чем в два раза. однажды, обеспечивая какой-то грубый полиморфизм в C.

    Вот практический пример: стреляй-себе-в-ногу площадка для линейных конгруэнтных генераторов псевдослучайных чисел. Поскольку реализация является локальной для блока компиляции, каждый блок компиляции получит свои собственные копии PRNG. Этот пример также показывает, как грубый полиморфизм может быть реализован в C.

    Прнг32.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
    

    Пример использования приведенного выше, пример-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;
    }
    

    Причина пометки как переменной _state, так и функции _next() static заключается в том, что таким образом каждая единица компиляции, включающая файл заголовка, имеет свою собственную копию переменных и функций-здесь свою собственную копию PRNG. Каждый из них, конечно, должен быть посеян отдельно; и если посеять его с одинаковой величиной, то получится та же последовательность.

    Следует вообще избегать таких попыток полиморфизма в С, потому что это приводит к сложные макропроцессорные махинации с препроцессором делают реализацию намного сложнее понять, поддерживать и модифицировать, чем это необходимо.

    Однако, когда исследует пространство параметров некоторого алгоритма-как здесь, типы 32-битных линейных конгруэнтных генераторов, это позволяет нам использовать одну реализацию для каждого из рассматриваемых генераторов, гарантируя отсутствие различий в реализации между ними. Заметьте, что даже этот случай больше похож на развитие событий инструмент, а не то, что вы должны видеть в реализации, предоставленной для использования другими.

  1. Если заголовок реализует простые static inline функции доступа

    Макросы препроцессора обычно используются для упрощения кода, обращающегося к сложным типам структур. static inline функции похожи, за исключением того, что они также обеспечивают проверку типов во время компиляции и могут ссылаться на свои параметры несколько раз (с помощью макросов, что проблематично).

    Один практический пример использования-это простой интерфейс для чтения файлов с использованием низкоуровневого POSIX.1 ввод / вывод (используя <unistd.h> и <fcntl.h> вместо <stdio.h>). Я сам делал это при чтении очень больших (от десятков мегабайт до гигабайт) текстовых файлов, содержащих вещественные числа (с пользовательским парсером float/double), поскольку стандартный ввод-вывод GNU C не особенно быстр.

    Например, 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 */
    

    Обратите внимание, что приведенные выше inbuffer_skip() и inbuffer_getc() не проверяют, является ли ib ненулевым; это типично для таких функций. Предполагается, что эти функции доступа являются "в быстром пути", т. е. вызываются очень часто. В таких случаях даже накладные расходы на вызов функции имеют значение (и избегаются функциями static inline, поскольку они дублируются в коде на месте вызова).

    Тривиальные функции доступа, такие как вышеупомянутые inbuffer_skip() и inbuffer_getc(), также могут позволить компилятору избежать перемещения регистров, участвующих в вызовах функций, поскольку функции ожидают, что их параметры будут расположены в определенных регистры или в стеке, тогда как встроенные функции могут быть адаптированы (wrt. зарегистрируйте использования), в код, окружающие подставляемые функции.

    Лично я рекомендую сначала написать несколько тестовых программ, использующих не встроенные функции, и сравнить производительность и результаты с встроенными версиями. Сравнение результатов гарантирует, что встроенные версии не имеют ошибок (выкл по одному типу здесь распространен!), и сравнение производительности и сгенерированных двоичных файлов (по крайней мере, размер) говорит вам стоит ли вообще вкладывать деньги.

Зачем вам нужна как глобальная, так и статическая функция? В языке c функции по умолчанию являются глобальными. Статические функции используются только в том случае, если требуется ограничить доступ к функции объявленным файлом. Поэтому вы активно ограничиваете доступ, объявляя его статическим...

Единственное требование для реализаций в заголовочном файле, это для функций шаблона c++ и функций-членов класса шаблона.

Comments

    Ничего не найдено.