iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐷

Inline Functions in C: Including Differences Between C99 and GNU89

に公開

Since C99, C has a feature called inline functions. While the feature itself is well-known, I believe the usage of not using it with static is surprisingly less known (at least, the free version of ChatGPT gave a nonsensical answer on this matter). Therefore, in this article, I will delve deeper into inline functions in C. Please note that this article targets C only, not C++.

Basics of Function Definition, Compilation, and Linking

First, let's review the basics of programs consisting of multiple files and function definitions.

Let's write a program consisting of foo1.c and main1.c as follows:

foo1.c
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main1.c
#include <stdio.h>

extern int add(int a, int b);
extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}
$ cc -o exe1 foo1.c main1.c
$ ./exe1
main: 0x102433ec0, 8
foo: 0x102433ec0, 8

The function addresses may change depending on the environment or for each execution, but otherwise, it is quite normal.

In C terminology, main1.c and foo1.c are different translation units (C17 5.1.1.1). Since add and foo have external linkage (C17 6.2.2), they refer to the same entity even across different translation units. The fact that the address of add is the same is proof of this.

Next, let's change the definition of add to include static:

foo2.c
#include <stdio.h>

static int add(int a, int b)
{
    return a + b;
}

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main2.c
#include <stdio.h>

extern int add(int a, int b);
extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}

Compiling and linking this will result in the following error:

$ cc -o exe2 foo2.c main2.c
Undefined symbols for architecture arm64:
  "_add", referenced from:
      _main in main2-846628.o
      _main in main2-846628.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

In C terminology, the definition of add in foo2.c now has internal linkage due to the addition of static. On the other hand, add in main2.c remains as external linkage. Since the definition of add (the one with external linkage) does not exist in the program despite being used, it results in an error (C17 6.9 paragraph 5).

Next, let's try defining add with static in main3.c:

foo3.c
#include <stdio.h>

static int add(int a, int b)
{
    return a + b;
}

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main3.c
#include <stdio.h>

static int add(int a, int b)
{
    return a + b;
}

extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}
$ cc -o exe3 foo3.c main3.c
$ ./exe3
main: 0x104157f50, 8
foo: 0x104157ee0, 8

This time, the compilation succeeded, but the address of add displayed in main and the address of add displayed in foo are different.

In C terminology, both the add in foo3.c and the add in main3.c have internal linkage, so they are allowed to have different entities for each translation unit. However, since the function contents are the same in this case, there might be a possibility that they are merged into the same entity by link-time optimization (though I'm not certain).

Finally, let's try defining the same function without static in two source files.

foo4.c
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main4.c
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}
$ cc -o exe4 foo4.c main4.c
duplicate symbol '_add' in:
    /private/var/folders/yg/36z4_5q142d6s6sn2y53gsd00000gn/T/main4-74bb37.o
    /private/var/folders/yg/36z4_5q142d6s6sn2y53gsd00000gn/T/foo4-3ec783.o
ld: 1 duplicate symbols
clang: error: linker command failed with exit code 1 (use -v to see invocation)

A link error occurred.

In C terminology, add has external linkage, but since multiple external definitions exist in the program, it results in an error. There must be at most one external definition for a single identifier with external linkage (exactly one if that identifier is actually used).

I will leave the general theory to the C standard, but roughly speaking, a function having external linkage refers to a function without static.

Also, a function definition being an external definition means it is not an inline definition. It is an error if multiple external definitions for the same identifier exist in a single program (C17 6.9 paragraph 5).

What is an Inline Function?

By adding the inline function specifier to a function, that function becomes an inline function, which may speed up function calls. Typically, this performance gain is achieved through inline expansion, but please note that it is not guaranteed to be inlined. The specific details are often implementation-defined or unspecified in the C standard.

Let's look into it in more detail.

First, functions with internal linkage (functions with static) can be specified as inline. For example:

static inline int add(int a, int b)
{
    return a + b;
}

You can also write it like this, as long as it is known to have internal linkage:

static int add(int a, int b); // Declare with internal linkage

int main(void)
{
    printf("%d\n", add(3, 5));
}

inline int add(int a, int b)
{
    return a + b;
}

Of course, whether it is actually inlined depends on the implementation.

When specifying inline for a function with external linkage, the behavior changes depending on the presence of extern. According to C17 6.7.4 paragraph 7, for a function with external linkage:

  • A function declared with the inline function specifier must have a definition in the same translation unit.
  • If all file-scope declarations of a function are "with the inline function specifier and without extern", the definition in that translation unit becomes an inline definition.
    • An inline definition does not provide an external definition.
    • Having an inline definition of an identifier in one translation unit does not prevent an external definition of that identifier from existing in another translation unit.
    • For function calls, the inline definition might be used instead of the external definition (how this actually happens is unspecified).

For example, the following code will result in an error:

main5.c
#include <stdio.h>

inline int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
}
$ cc -oexe5 -std=c17 main5.c
Undefined symbols for architecture arm64:
  "_add", referenced from:
      _main in main5-9ea94f.o
      _main in main5-9ea94f.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

The reason is that add in main5.c has external linkage and is specified with inline but without extern, so it does not provide an external definition.

On the other hand, the following code will compile:

main6.c
#include <stdio.h>

extern inline int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
}
$ cc -oexe6 -std=c17 main6.c
$ ./exe6
main: 0x1047ebf18, 8

In this case, add does not fall under the category of an inline definition, so the C standard says nothing about whether inlining occurs. However, since the rule states that there can be at most one external definition of an identifier with external linkage in the program, there is no obstacle for the implementation to perform optimizations such as inlining.

If there is even one declaration without inline in the translation unit, it will no longer fall under the category of an inline definition and will provide an external definition:

main7.c
#include <stdio.h>

inline int add(int a, int b)
{
    return a + b;
}

extern int add(int a, int b); // Declaration without inline

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
}
$ cc -oexe7 -std=c17 main7.c
$ ./exe7
main: 0x100c47f18, 8

How to Combine Headers and Source Files: The Case for Standard C

When writing a library in C, it seems beneficial to provide an inline definition for a certain function while also preparing an external definition for cases where inlining is not possible (for example, when you want to take the address or call it from a language other than C). Let's consider how to write it in such a case.

First, define the function as an inline function without static or extern in the header:

add.h
#if !defined(ADD_H)
#define ADD_H

inline int add(int a, int b)
{
    return a + b;
}

#endif

If it is acceptable for different entities to be generated for each translation unit and external linkage is not required, you can add static, but here we assume we want external linkage.

Since an inline definition alone does not generate an external definition, you provide a declaration of add with extern in one of the source files (translation units) (note that this must be a declaration, not a definition):

add.c
#include "add.h"

extern int add(int a, int b);
/* If you write the definition here like:
extern int add(int a, int b)
{
    return a + b;
}
it would violate the "at most one external definition" rule.
*/
/*
If you write:
inline int add(int a, int b);
here, an external definition will not be generated.
 */
/*
A declaration without extern, such as:
int add(int a, int b);
is also fine.
 */
/*
If for some reason you want to adopt a different definition for the inline definition and the non-inline one, you can write:
int add(int a, int b)
{
    ...
}
without including "add.h".
 */

The user side simply includes add.h:

foo8.c
#include <stdio.h>
#include "add.h"

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main8.c
#include <stdio.h>
#include "add.h"

/* If you write:
extern int add(int a, int b);
here, an external definition will be generated and it will conflict with add.c.
 */

extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}
$ cc -oexe8 -std=c17 main8.c foo8.c add.c
$ ./exe8
main: 0x102917f58, 8
foo: 0x102917f58, 8

Successfully compiled with only one external definition generated.

I believe this form is the best practice, but I feel like I don't see it very often. Most people probably use static inline. With static inline, everything is self-contained in the header.

About GNU89 and __attribute__((gnu_inline))

GCC has implemented inline functions as its own extension since the C89 era. Its specification is slightly different from that of C99, so caution is required. The old specification is enabled by passing the -std=gnu89 or -fgnu89-inline option to GCC, or by adding the gnu_inline attribute to the function. Here, we will call this old specification "GNU89 mode."

Reference: "Inline (Using the GNU Compiler Collection (GCC))"

In GNU89 mode, an inline function without both static and extern provides an external definition.

In GNU89 mode, functions with extern inline are used only for inline expansion and do not provide an external definition. It can be thought of as equivalent to the C standard's "inline definition."

For example, in the following code, an external definition is provided by foo, and add refers to the same entity in both main and foo:

foo-gnu89.c
#include <stdio.h>

__attribute__((gnu_inline))
inline int add(int a, int b)
{
    return a + b;
}

void foo(void)
{
    printf("foo: %p, %d\n", add, add(3, 5));
}
main-gnu89.c
#include <stdio.h>

extern int add(int a, int b);
extern void foo(void);

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
    foo();
}
$ gcc -oexe-gnu89 main-gnu89.c foo-gnu89.c
$ ./exe-gnu89
main: 0x5650472dd18c, 8
foo: 0x5650472dd18c, 8

On the other hand, the following example results in a compilation error because there is no external definition of add:

main-gnu89-bad.c
#include <stdio.h>

__attribute__((gnu_inline))
extern inline int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
}
$ gcc main-gnu89-bad.c
/usr/bin/ld: /tmp/ccd9D6OW.o: in function `main':
main-gnu89-bad.c:(.text+0x13): undefined reference to `add'
/usr/bin/ld: main-gnu89-bad.c:(.text+0x1c): undefined reference to `add'
collect2: error: ld returned 1 exit status

Providing an inline declaration without extern will cause it to provide an external definition:

main-gnu89-2.c
#include <stdio.h>

__attribute__((gnu_inline))
extern inline int add(int a, int b)
{
    return a + b;
}

__attribute__((gnu_inline))
inline int add(int a, int b); // inline declaration without extern

int main(void)
{
    printf("main: %p, %d\n", add, add(3, 5));
}
$ gcc main-gnu89-2.c
$ ./a.out
main: 0x55a4b4022149, 8

Whether the current compilation is in GNU89 or C99 mode can be determined using the __GNUC_STDC_INLINE__ and __GNUC_GNU_INLINE__ macros in GCC 4.2 and later (Reference: Common Predefined Macros (The C Preprocessor)).

#include <stdio.h>
int main()
{
#if defined(__GNUC_STDC_INLINE__)
    puts("__GNUC_STDC_INLINE__");
#endif
#if defined(__GNUC_GNU_INLINE__)
    puts("__GNUC_GNU_INLINE__");
#endif
}

How to Combine Header and Source Files: When Considering GNU89 Mode

Just like before, suppose you want to provide both an inline definition and an external definition when writing a library in C. Since the meaning of inline differs between C99 and GNU89, some ingenuity is required when considering GNU89 mode.

One option is to assume that GNU89 mode is not widely used as of 2025 and simply not provide an inline definition in GNU89 mode. In that case, the header description would look like this:

add.h
#if defined(__GNUC_GNU_INLINE__)
// GNU89 mode
extern int add(int a, int b);
#else
// Standard C
inline int add(int a, int b)
{
    return a + b;
}
#endif

For reference, the default for GCC has been -std=gnu11 since GCC 5 (released in 2015), and it seems that __GNUC_GNU_INLINE__ became available in GCC 4.2. If you need to consider even older versions of GCC, more detailed conditions will be required.

If you want to enable inline definitions even in older versions of GCC, you can switch between extern inline and inline accordingly.

// Header description
#if defined(__GNUC_GNU_INLINE__)
// GNU89 mode
extern inline
#else
// C99 mode
inline
#endif
int add(int a, int b)
{
    return a + b;
}
// Source description
#include "add.h"
#if defined(__GNUC_GNU_INLINE__)
inline
#else
extern
#endif
int add(int a, int b);

Comparison of inline in C99, GNU89, and C++

C99 introduced an inline that is slightly different from both C++ and GNU89. It cannot be denied that this has caused confusion (I myself thought "C inline seems troublesome" until I wrote this article). Was the incompatibility worth it?

Reading the document when C99's inline was formulated (ISO/IEC JTC1/SC22/WG14 N709), it shows that compatibility with C++ was actually emphasized. Specifically, code written assuming C99's inline can be interpreted as C++ as it is.

In C++, even if multiple instances of the same function are generated in different translation units, the linker is designed to select them appropriately. The reason C99 did not follow the C++ way and required an external definition to be placed somewhere is likely because they wanted to make it implementable even in environments that do not support features like weak symbols (N709 mentions "such a way that it can be implemented with existing linker technology").

Therefore, my conclusion is that the specification of C99's inline is not that bad.

Implementation-Specific Features: always_inline, forceinline

The C standard's inline is merely a hint to the compiler, and there is no guarantee that inlining will actually occur. Depending on the compiler, more forceful attributes may be provided.

GCC and compatible compilers (such as Clang) can force function calls to be inlined using the always_inline attribute: Common Function Attributes (Using the GNU Compiler Collection (GCC))

MSVC provides the __forceinline keyword: Inline Functions (C++) | Microsoft Learn. However, it is positioned as a "strong hint" and does not seem to truly force it.

For details, please refer to the manual of each compiler.

Terminology

I will list the locations of the definitions for related terms in C17. Section and paragraph numbers refer to those in C17.

  • Translation unit 5.1.1.1
  • Linkage 6.2.2
    • External linkage
    • Internal linkage
    • No linkage
  • Declaration 6.7
  • Definition 6.7 paragraph 5
  • Storage-class specifier 6.7.1
    • typedef, extern, static, _Thread_local, auto, register
  • Function specifier 6.7.4
    • inline, _Noreturn
    • Inline definition 6.7.4
  • External declaration 6.9
  • External definition 6.9 paragraph 5

References

Discussion