Search code examples
clanguage-lawyerc23

Can the expanded code of an inline function differ between 2 translation units?


Say I have a macro UNIQUE_NAME(PREFIX) that concatenates __LINE__ to PREFIX:

#define CONCAT2_EXPAND(a, b)    a ## b
#define CONCAT2(a, b)           CONCAT2_EXPAND(a, b)
#define UNIQUE_NAME(prefix)     CONCAT2(CONCAT2(prefix, _), __LINE__)

Now if this macro was to be used in an inline function, included in two different translation units, then the expanded code of the inline function would differ between the two translation units (unless they have the same includes).

Is this behavior defined, unspecified, implementation-defined, or undefined?


This is what I got during a code review:

You're caught between a rock and a hard place here. Using __LINE__ is a bad idea, because there's nothing preventing me from calling UNIQUE_NAME twice on the same line, and then it's not really unique any longer, is it? __COUNTER__ would give a unique name, but it'd be too unique: if UNIQUE_NAME was used in an inline function, included in two different translation units, then the expanded code of the inline function would differ between the two TUs (unless they have the same includes), which is Undefined Behavior. – @Matthieu M.

If it helps, I am confused about having a static inline function and an inline function (if it matters which one) in a header file and including it in different .c files.

Say:

[[gnu::always_inline]] static inline void foo(void)
{
    bool UNIQUE_NAME(bar);
    // ...
}

and

inline void foo(void)
{
    bool UNIQUE_NAME(bar);
    // ...
}

I believe the reviewer meant the second one, and not the first one.


Solution

  • If you use static inline, then everything is fine. Then you simply have (conceptually) a static function in each translation unit, and they are fully independent of each other; the same way that static functions have always worked in C. Adding inline doesn't change these semantics; the difference between static and static inline is effectively just a hint to the compiler that inlining might be a good idea, but it remains free to inline or not, with or without the keyword.

    If you just use inline, now the function has external linkage and you have created an inline definition under 6.7.4p7. This is not an external definition of foo(), and it still needs to have one under 6.9p5, so you'll have to provide one elsewhere. This could be done by including a definition of foo (without inline) in some third translation unit which does not include the header. Or, by adding the declaration extern void foo(void); to exactly one of the translation units which does include the header, in which case the definition in that translation unit is no longer an inline definition (despite the inline keyword) and becomes a regular external definition. See Example 1 in 6.7.4p10.

    This does have a practical advantage (in a typical implementation) in that if there are instances where the function cannot be inlined, then all such instances from all translation units can call the same single out-of-line copy of the function. If you define it static inline then you may get a separate out-of-line copy per translation unit, which makes your executable larger, increases memory usage, makes caching less efficient, etc (though the effects may or may not actually be significant).

    Note that on typical implementations, if the compiler actually is able to inline every instance of the function, it may never actually need to look for the external definition, and thus may not complain if it doesn't exist. But the standard still requires it.

    As far as I know, C doesn't actually require that the inline definitions agree with each other, nor with the external definition. However, when they both exist, then in any given place where the function is called, it is unspecified which definition is actually executed, and this could lead to confusion.

    The reviewer might be thinking about the rule that a function cannot have more than one external definition (whether they are identical or not). However, as it stands you don't have multiple external definitions; in fact you don't have any (which is still bad but for a different reason). If you add an external definition in one translation unit, then you'll have exactly one, which is what's required.

    The reviewer might also be thinking of C++ which does have a rule that inline definitions of a function must all be identical (C++20 6.3p13). But as far as I know, C does not have such a rule.