52

For me it's a rule to define and declare static functions inside source files, I mean .c files.

However in very rare situations I saw people declaring it in the header file. Since static functions have internal linkage we need to define it in every file we include the header file where the function is declared. This looks pretty odd and far from what we usually want when declaring something as static.

On the other hand if someone naive tries to use that function without defining it the compiler will complaint. So in some sense is not really unsafe to do this even sounding strange.

My questions are:

  • What is the problem of declaring static functions in header files?
  • What are the risks?
  • What the impact in compilation time?
  • Is there any risk in runtime?
Daniel
  • 10,641
  • 12
  • 47
  • 85
miguel azevedo
  • 537
  • 1
  • 4
  • 5
  • I would rather ask: What could be an acceptable reason to do that? IMO it's definitely code smell. – Erich Kitzmueller Feb 05 '17 at 19:26
  • 7
    Small `static inline` functions are occasionally used as accessor functions into structure data types, as a (better) replacement for preprocessor macros. Unlike macros, static inline functions provide compile time type checking, and can refer to their parameters multiple times (which is problematic with preprocessor macros). – Nominal Animal Feb 05 '17 at 20:20
  • 1
    @NominalAnimal Good point. Seeing the source code allows the compiler to perform better optimizations in general (inlining being one of them) without linker help. – Peter - Reinstate Monica Feb 05 '17 at 22:25
  • Can anybody say what the size impact of defining the same function over and over again in many compilation units is? I assume the linker (assuming different TUs) cannot fold them in one so the object code is multiplied? – Peter - Reinstate Monica Feb 05 '17 at 22:28
  • @PeterA.Schneider: For macro replacement static inline functions, minimal. Function calls tend to need some register shuffling (to pass the parameters), whereas many compilers can avoid such for static inline functions (and instead work on the registers that have the data already). It varies greatly, of course, but I'd say that if inlining the function significantly increases the code size, the function should not be inlined in the first place. – Nominal Animal Feb 05 '17 at 22:43
  • @NominalAnimal I didn't mean (necessarily) inline functions. Just generally: The object code of a (not inlined) static function will obviously reside in each object file in which it is defined. Are linkers (say, gnu ld) smart enough to eliminate the duplicates? – Peter - Reinstate Monica Feb 05 '17 at 23:13
  • I would say that you should never declare a static function in the header, without also defining in the header. (There are reasons to define a static function in the header however). – M.M Feb 05 '17 at 23:21
  • If the function is called but not defined, it is undefined behaviour with no diagnostic required. But most linkers will give an error. – M.M Feb 05 '17 at 23:23
  • @PeterA.Schneider: GCC-5.4 and binutils-2.26 on x86-64 is very keen to actually inline functions marked `static`, so I had to add the `noinline` function attribute (a GCC extension) to verify no-inline static function linkage; this (artificially!) resulted in exact duplicates of the static functions being linked in to the binary. I tested both `-Os` and `-O2`. However, adding `-flto` to enable link time optimizations combined my three non-inlined `static` test function objects into a single code chunk! – Nominal Animal Feb 06 '17 at 09:54
  • @NominalAnimal Re -flto: Interesting, wouldn't have thought that. Thanks for investigating. – Peter - Reinstate Monica Feb 06 '17 at 11:22

3 Answers3

42

First I'd like to clarify my understanding of the situation you describe: The header contains (only) a static function declaration while the C file contains the definition, i.e. the function's source code. For example

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()

If this is the situation you describe, I take issue with your remark

Since static functions have internal linkage we need to define it in every file we include the header file where the function is declared.

If you declare the function but do not use it in a given translation unit, I don't think you have to define it. gcc accepts that with a warning; the standard does not seem to forbid it, unless I missed something. This may be important in your scenario because translation units which do not use the function but include the header with its declaration don't have to provide an unused definition.


Now let's examine the questions:
  • What is the problem of declaring static functions in header files?
    It is somewhat unusual. Typically, static functions are functions needed in only one file. They are declared static to make that explicit by limiting their visibility. Declaring them in a header therefore is somewhat antithetical. If the function is indeed used in multiple files with identical definitions it should be made external, with a single definition. If only one translation unit actually uses it, the declaration does not belong in a header.

    One possible scenario therefore is to ensure a uniform function signature for different implementations in the respective translation units. The common header leads to a compile time error for different return types in C (and C++); different parameter types would cause a compile time error only in C (but not in C++' because of function overloading).
  • What are the risks?
    I do not see risks in your scenario. (As opposed to also including the function definition in a header which may violate the encapsulation principle.)
  • What the impact in compilation time?
    A function declaration is small and its complexity is low, so the overhead of having additional function declarations in a header is likely negligible. But if you create and include an additional header for the declaration in many translation units the file handling overhead can be significant (i.e. the compiler idles a lot while it waits for the header I/O)
  • Is there any risk in runtime?
    I cannot see any.
Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
21

This is not an answer to the stated questions, but hopefully shows why one might implement a static (or static inline) function in a header file.

I can personally only think of two good reasons to declare some functions static in a header file:


  1. If the header file completely implements an interface that should only be visible in the current compilation unit

    This is extremely rare, but might be useful in e.g. an educational context, at some point during the development of some example library; or perhaps when interfacing to another programming language with minimal code.

    A developer might choose to do so if the library or interaface implementation is trivial and nearly so, and ease of use (to the developer using the header file) is more important than code size. In these cases, the declarations in the header file often use preprocessor macros, allowing the same header file to be included more than once, providing some sort of crude polymorphism in C.

    Here is a practical example: Shoot-yourself-in-the-foot playground for linear congruential pseudorandom number generators. Because the implementation is local to the compilation unit, each compilation unit will get their own copies of the PRNG. This example also shows how crude polymorphism can be implemented in 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
    

    An example using the above, 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;
    }
    

    The reason for marking both the _state variable and the _next() function static is that this way each compilation unit that includes the header file has their own copy of the variables and the functions -- here, their own copy of the PRNG. Each must be separately seeded, of course; and if seeded to the same value, will yield the same sequence.

    One should generally shy away from such polymorphism attempts in C, because it leads to complicated preprocessor macro shenanigans, making the implementation much harder to understand, maintain, and modify than necessary.

    However, when exploring the parameter space of some algorithm -- like here, the types of 32-bit linear congruential generators, this lets us use a single implementation for each of the generators we examine, ensuring there are no implementation differences between them. Note that even this case is more like a development tool, and not something you ought to see in a implementation provided for others to use.


  1. If the header implements simple static inline accessor functions

    Preprocessor macros are commonly used to simplify code accessing complicated structure types. static inline functions are similar, except that they also provide type checking at compile time, and can refer to their parameters several times (with macros, that is problematic).

    One practical use case is a simple interface for reading files using low-level POSIX.1 I/O (using <unistd.h> and <fcntl.h> instead of <stdio.h>). I've done this myself when reading very large (dozens of megabytes to gigabytes range) text files containing real numbers (with a custom float/double parser), as the GNU C standard I/O is not particularly fast.

    For example, 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 */
    

    Note that the above inbuffer_skip() and inbuffer_getc() do not check if ib is non-NULL; this is typical for such functions. These accessor functions are assumed to be "in the fast path", i.e. called very often. In such cases, even the function call overhead matters (and is avoided with static inline functions, since they are duplicated in the code at the call site).

    Trivial accessor functions, like the above inbuffer_skip() and inbuffer_getc(), may also let the compiler avoid the register moves involved in function calls, because functions expect their parameters to be located in specific registers or on the stack, whereas inlined functions can be adapted (wrt. register use) to the code surrounding the inlined function.

    Personally, I do recommend writing a couple of test programs using the non-inlined functions first, and compare the performance and results to the inlined versions. Comparing the results ensure the inlined versions do not have bugs (off by one type is common here!), and comparing the performance and generated binaries (size, at least) tells you whether inlining is worth it in general.

Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • It struck me as odd in example 1 that you give "should be visible only in this TU" as a reason for putting something in a header file! Usually headers are used for the very opposite. But I see your use case here: You define a configurable framework identical to (i.e *shared by!*) all using TUs, but each TU can configure the mechanism to its particular needs. – Peter - Reinstate Monica Feb 06 '17 at 11:29
  • 1
    @miguelazevedo The canonical way to express gratitude on StackOverflow is to upvote and/or mark as the accepted answer ;-). – Peter - Reinstate Monica Feb 06 '17 at 11:33
  • @Peter A. Schneider i tried but looks I don't have that privilege – miguel azevedo Feb 06 '17 at 12:30
  • @PeterA.Schneider: Exactly. – Nominal Animal Feb 06 '17 at 12:42
  • @miguelazevedo: It's because you are a new member; at Stack Overflow and related sites, you get additional [privileges](http://stackoverflow.com/help/privileges) as you gain reputation. For example, to upvote a post you need to have 15 reputation or more. So, don't worry about upvoting just yet. Besides, you can always come back and upvote posts later on -- you can only upvote each answer once, after all. – Nominal Animal Feb 06 '17 at 12:45
1

Why would you want a both global and static function? In c, functions are global by default. You only use static functions if you want to limit the access to a function to the file they are declared. So you actively restrict access by declaring it static...

The only requirement for implementations in the header file, is for c++ template functions and template class member functions.

JHBonarius
  • 10,824
  • 3
  • 22
  • 41