c++lambdac++20one-definition-rule

How does ODR work with lambdas at global scope


I have the following code -

template <typename T, typename F>
struct unique {};

template <auto F> 
struct ltype {
   using type = decltype(F);
};

using p = unique<int, ltype<[](){}>::type>;
using q = unique<int, ltype<[](){}>::type>;

void foo(p x);
void foo(q x);

I am using an instance of a lambda to create an unnamed type. I want to understand how linkages work for foo and if the declaration appears in a header file would it violate the one definition rule?

I tried to look at the mangled name of the foo(s) and they just differ by a unique counter. Output from nm (if the two declarations are defined) -

000000000000000b t _Z3foo6uniqueIiKUlvE0_E
0000000000000000 t _Z3foo6uniqueIiKUlvE_E

Does this mean if the header is included in a different order in different translation units, foo would get a different name and the linker would complain?

I understand if I was actually defining a lambda at the global scope in a header that would be a problem, but I am not instantiating one here, just using it as a template parameter.

[Hoisted from the OP's comments]

My question was about the case where this code is in a header and is included in two translation units. I was also referring to the case where one of the translation unit defines these functions and the other calls them. Since the types are unique to the translation unit, would the definition [not] match the call?


Solution

  • (To readers who didn't get there already, do consider @YurkoFlisk's added rejoinder to my answer.)

    You explain in a comment:

    my question was about the case where this code is in a header and is included in two translation units. I was also referring to the case where one of the translation unit defines these functions and the other calls them. Since the types are unique to the translation unit, would the definition [not] match the call?

    Let's try it and see! I'll save considerations of compliance with the C++20 Standard for later.

    The source files:-

    $ tail -n +1 foo.h foo.cpp main.cpp
    ==> foo.h <==
    #ifndef FOO_H
    #define FOO_H
    
    #ifndef THIS_FILE
    #define THIS_FILE __FILE__
    #endif
    
    template <typename T, typename F>
    struct unique {};
    
    template <auto F> 
    struct ltype {
       using type = decltype(F);
    };
    
    using p = unique<int, ltype<[](){}>::type>;
    using q = unique<int, ltype<[](){}>::type>;
    
    void foo(p x);
    void foo(q x);
    
    #endif
    
    ==> foo.cpp <==
    #include "foo.h"
    #include <cstdio>
    #include <typeinfo>
    
    void foo(p x)  {
        (void) x;
        std::printf("In file %s, the type of \"void foo(p)\" is %s\n",
            THIS_FILE,typeid(void (p)).name());
    }
    
    void foo(q x) {
        (void)x;
        std::printf("In file %s, the type of \"void foo(q)\" is %s\n",
            THIS_FILE,typeid(void (q)).name());
    }
    
    ==> main.cpp <==
    #include "foo.h"
    
    int main()
    {
        foo(p());
        foo(q());
        return 0;
    }
    

    I'm using:

    $ g++ --version | head -n1
    g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
    

    but I'll give other compilers their say.

    Compile foo.cpp, the one that defines the foo overloads:

    $ g++ -c -std=c++20 -Wall -Wextra -pedantic foo.cpp
    foo.cpp:11:6: warning: ‘void foo(q)’ defined but not used [-Wunused-function]
       11 | void foo(q x) {
          |      ^~~
    foo.cpp:5:6: warning: ‘void foo(p)’ defined but not used [-Wunused-function]
        5 | void foo(p x)  {
          |      ^~~
    

    Interesting warnings. Syntactically the foos are global functions: not static, not in the scope of an anonymous namespace. They should have external linkage. But a unused function warning is only issued for a function with internal linkage that is not called in the translation unit - a static function or one defined in an anonymous namespace.

    Let's have a look at the symbol table of the object file we've got:

    $ readelf -sWC foo.o
    
    Symbol table '.symtab' contains 15 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.cpp
         2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 .text
         3: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 .text._ZNKSt9type_info4nameEv
         4: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 .rodata
         5: 0000000000000000    59 FUNC    LOCAL  DEFAULT    2 _Z3foo6uniqueIiUlvE_E
         6: 0000000000000010    16 OBJECT  LOCAL  DEFAULT    8 _ZTIFv6uniqueIiUlvE_EE
         7: 000000000000003b    59 FUNC    LOCAL  DEFAULT    2 _Z3foo6uniqueIiUlvE0_E
         8: 0000000000000000    16 OBJECT  LOCAL  DEFAULT    8 _ZTIFv6uniqueIiUlvE0_EE
         9: 0000000000000070    21 OBJECT  LOCAL  DEFAULT    7 _ZTSFv6uniqueIiUlvE0_EE
        10: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 .data.rel.ro
        11: 0000000000000090    20 OBJECT  LOCAL  DEFAULT    7 _ZTSFv6uniqueIiUlvE_EE
        12: 0000000000000000    51 FUNC    WEAK   DEFAULT    6 std::type_info::name() const
        13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
        14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND vtable for __cxxabiv1::__function_type_info
         
    

    Yes indeed, the foo's appear in the symbol table as LOCAL function symbols ( meaning internal linkage)_Z3foo6uniqueIiUlvE_E and _Z3foo6uniqueIiUlvE0_E, just as if they were static or defined in an anonymous namespace scope. And note that even though we requested demangling of the C++ symbols (-C|--demangle), we didn't get it for any symbol whose C++ type depends on the lambdas: there is no demangling of those symbols because a lambda has an unnamed type with no constant mangling.

    So before we even attempt any linkage we already know that these definitions cannot be referenced from any other object file. foo.o is useless for resolving external references to the foos.

    Now let's compile main.cpp, the one where the foos are called.

    $ g++ -c -std=c++20 -Wall -Wextra -pedantic main.cpp
    In file included from main.cpp:1:
    foo.h:20:6: warning: ‘void foo(q)’ used but never defined
       20 | void foo(q x);
          |      ^~~
    foo.h:19:6: warning: ‘void foo(p)’ used but never defined
       19 | void foo(p x);
          |      ^~~
    

    Again warnings that chime with the first ones. These warnings should only be issued if the foos are functions that are declared, without definition, and have internal linkage. Then the compiler, having warned, disregards the declarations that have internal linkage and assumes that the calls are to undeclared, undefined external functions for which the linker will find definitions in some other file. Let's check out the new object file:

    $ readelf -sWC main.o
    
    Symbol table '.symtab' contains 6 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.cpp
         2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
         3: 0000000000000000    25 FUNC    GLOBAL DEFAULT    1 main
         4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z3foo6uniqueIiUlvE_E
         5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z3foo6uniqueIiUlvE0_E
         
    

    Yes, the foo symbols are undefined external (GLOBAL) references. And there is no way that foo.o can resolve those references at linktime, because its definitions will be unavailable for linkage.

    You'll get equivalent diagnostics with:

    You'll see that clang and msvc issue more explicit diagnostics than GCC about the linkage of the foos:

    From main.cpp with clang:

    <source>:19:6: warning: function 'foo' has internal linkage but is not defined [-Wundefined-internal]
       19 | void foo(p x);
          |      ^
    <source>:26:2: note: used here
       26 |         foo(p());
          |         ^
    <source>:20:6: warning: function 'foo' has internal linkage but is not defined [-Wundefined-internal]
       20 | void foo(q x);
          |      ^
    <source>:27:2: note: used here
       27 |         foo(q());
          |         ^
        
    

    From foo.cpp with msvc:

    <source>(28): warning C5245: 'foo': unreferenced function with internal linkage has been removed
    <source>(34): warning C5245: 'foo': unreferenced function with internal linkage has been removed
    <source>(17): warning C5264: ' ?? <lambda_1_>{}': 'const' variable is not used
    <source>(18): warning C5264: ' ?? <lambda_2_>{}': 'const' variable is not used
    

    (msvc deleted the foos from the object file, a routine elimination of unreachable code).

    From main.cpp with msvc:

    <source>(21): warning C5046: 'foo': Symbol involving type with internal linkage not defined
    <source>(20): warning C5046: 'foo': Symbol involving type with internal linkage not defined
    <source>(17): warning C5264: ' ?? <lambda_1_>{}': 'const' variable is not used
    <source>(18): warning C5264: ' ?? <lambda_2_>{}': 'const' variable is not used
    

    The Big Three compilers are unanimous that the definitions of the foos have internal linkage. That means, as far as they are concerned, the ODR works with lambdas at global scope by being rendered irrelevant, and the answer to the question:

    Since the types are unique to the translation unit, would the definition [not] match the call?

    is that definitions in one TU will not match the calls in another TU simply because the linker cannot even consider those LOCAL definitions. Let's put it to the test:

    $ g++ main.o foo.o
    /usr/bin/ld: main.o: in function `main':
    main.cpp:(.text+0x9): undefined reference to `_Z3foo6uniqueIiUlvE_E'
    /usr/bin/ld: main.cpp:(.text+0xe): undefined reference to `_Z3foo6uniqueIiUlvE0_E'
    collect2: error: ld returned 1 exit status
    

    But the premiss of that question is importantly wrong.

    The types of the foos are unique in each translation unit, because the types of the lambdas are unique per translation unit. But they are not thereby unique to a translation unit. Assuming that foo.h is included identically in each translation unit, and no preceding code influences the unique type-formation of the foo's, they can get the same types in each translation unit. Witness this program:

    $ tail -n +1 foobar.cpp foobaz.cpp barbaz.cpp
    ==> foobar.cpp <==
    #define THIS_FILE "foobar.cpp"
    #include "foo.cpp"
    
    int bar = (foo(p()),foo(q()),42);
    
    ==> foobaz.cpp <==
    #define THIS_FILE "foobaz.cpp"
    #include "foo.cpp"
    
    int baz = (foo(p()),foo(q()),42);
    
    ==> barbaz.cpp <==
    #include <cstdio>
    extern int bar;
    extern int baz;
    
    int main()
    {
        std::printf("bar=%d, baz=%d\n",bar,baz);
        return 0;
    }
    
    $ g++ -c barbaz.cpp foobar.cpp foobaz.cpp -std=c++20 -Wall -Wextra -pedantic
    $ g++ barbaz.o foobar.o foobaz.o
    $ ./a.out
    In file foobar.cpp, the type of "void foo(p)" is Fv6uniqueIiUlvE_EE
    In file foobar.cpp, the type of "void foo(q)" is Fv6uniqueIiUlvE0_EE
    In file foobaz.cpp, the type of "void foo(p)" is Fv6uniqueIiUlvE_EE
    In file foobaz.cpp, the type of "void foo(q)" is Fv6uniqueIiUlvE0_EE
    bar=42, baz=42
    

    The same sequence of TU-unique types is generated for the foo's in both foobar.cpp and foobaz.cpp. Unsurprising.

    This fact means it is not an option for compilers to emit global symbols for the foos if such a program is to link, for if they did, e.g. as per:

    $ objcopy --globalize-symbol _Z3foo6uniqueIiUlvE_E foobar.o foobar.glob.o
    $ objcopy --globalize-symbol _Z3foo6uniqueIiUlvE_E foobaz.o foobaz.glob.o
    $ g++ barbaz.o foobar.glob.o foobaz.glob.o
    /usr/bin/ld: foobaz.glob.o: in function `_Z3foo6uniqueIiUlvE_E':
    foobaz.cpp:(.text+0x0): multiple definition of `_Z3foo6uniqueIiUlvE_E'; foobar.glob.o:foobar.cpp:(.text+0x0): first defined here
    collect2: error: ld returned 1 exit status
    

    the ODR would be in broken in the typical way. They have the choices of emitting local symbols or (where the target supports it) weak symbols.

    The weak symbol course is the one taken by C++ compilers targetting ELF to define template instantiations, so that they have external linkage and are callable across TU boundaries, but it is not applicable for types compounded of lambda types, like the foos, because unlike template types the type of "the same" lamba or lambda-compounded type may vary just depending on what lambdas are defined before it in each translation unit, so that a reference to a lambda-compounded foo from TU A does not necessarily generate the same symbol as its definition in TU B - just the reason for not giving external linkage to such things at all.


    Now let's consider the C++20 Standard

    As ably presented in @YurkoFlisk's answer, the Standard mandates that the foo's have external linkage. By emitting local symbols for the foos the compilers appear prima facie to be foul the Standard. @YurkoFlisk argues that:

    the Standard doesn't care about how external/internal linkage is implemented, as long as observed behaviour is consistent with what the Standard specifies.

    I accept neither of these points.

    For a), the uniqueness of a lambda type as prescribed by the Standard at the cited expr.prim.lambda.closure cannot mean uniqueness in all the TUs you might at any moment choose to put into a linkage. That would be magic. It means uniqueness within any TU at the utmost. The Standard affords other instances where the adjective unique is used with implicit restriction to the contextually determined domain: in the context of types generated by the implementation this cannot be more comprehensive than the domain of types in a TU. We've already observed that GCC-generated lambda types are not unique across TUs and the same can be observed for clang and msvc. We've also already caused an ODR violation to occur when the foos are actually given external linkage.

    For b), we do not even need to have the implementation (the object code) and observe that the foos are emitted as local symbols to infer - rightly or wrongly - that the foos have internal linkage. For all the compilers it is implied by the warnings they issue and in the case of clang and msvc the warnings straight-out say that they have given the foos internal linkage.

    So I suggest that all the compilers are indeed foul of the Standard when they compile the definitions of the foos with internal linkage - and deliberately so: it's the sensible thing to do. It's not sensational for compilers to be willing to generate code that bucks the Standard, with a covering warning, even if we request compilation compliant to a standard. Especially around thorny novelties introduced by a standard, such as C++20's admission of class types as non-type template parameters, which is the nub of your problem code. We can coerce a warning to an error with -Werror or equivalent if we absolutely insist on compliance.


    TU-local entities

    I also suggest that the C++20 standard (and the C++23 one) are in a transitional state with respect to the legality of your code.

    The C++20 Standard introduced the concept of Translation-unit-local entities - TU-local, for short. The headline innovation of C++20 was modules and the TU-local language was motivated by the need to demarcate the external interface of a TU, when implementing a module, much less porously (non-porously) than was ever necessary when header files were the only vehicles for such demarcation.

    Per clause 2 of:

    An entity is TU-local if it is

    1. a type, function, variable, or template that
      1. has a name with internal linkage, or
      2. does not have a name with linkage and is declared, or introduced by a lambda expression, within the definition of a TU-local entity,
    2. a type with no name that is defined outside a class-specifier, function body, or initializer or is introduced by a defining-type-specifier (type-specifier, class-specifier or enum-specifier) that is used to declare only TU-local entities,
    3. a specialization of a TU-local template,
    4. a specialization of a template with any TU-local template argument, or
    5. a specialization of a template whose (possibly instantiated) declaration is an exposure (defined below).

    the type of your lambdas is TU-local.

    In association with the TU-local concept comes that of an exposure. From the same reference:

    Exposures

    A declaration D names an entity E if

    • D contains a lambda expression whose closure type is E,
    • E is not a function or function template and D contains an id-expression, type-specifier, nested-name-specifier, template-name, or concept-name denoting E, or
    • E is a function or function template and D contains an expression that names E or an id-expression that refers to a set of overloads that contains E.

    A declaration is an exposure if it either names a TU-local entity, ignoring

    • the function-body for a non-inline function or function template (but not the deduced return type for a (possibly instantiated) definition of a function with a declared return type that uses a placeholder type),
    • the initializer for a variable or variable template (but not the variable’s type),
    • friend declarations in a class definition, and
    • any reference to a non-volatile const object or reference with internal or no linkage initialized with a constant expression that is not an odr-use,

    or defines a constexpr variable initialized to a TU-local value.

    By which your foos make exposures of the TU-local lambdas.

    And then:

    TU-local constraints

    If a (possibly instantiated) declaration of, or a deduction guide for, a non-TU-local entity in a module interface unit (outside the private-module-fragment, if any) or module partition is an exposure, the program is ill-formed. Such a declaration in any other context is deprecated.

    My emphasis. The prohibition of exposures in module interface units is directed to the encapsulation problem for modules. Prohibiting them in all contexts would further prevent things that are TU-local and in particular have internal linkage being used to define things are not TU-local and ought to have external linkage, per past and present Standards. But modules do not yet have a substantial legacy codebase, and other contexts do. Hence only deprecation for the other contexts for now.

    The context in which your foos expose the TU-local lambdas fall into any other, and as such they deprecated.

    In the C++20 Standard public draft (and likewise the C++23 Standard), this deprecation is itemised at:

    D.7 Non-local use of TU-local entities [depr.local]

    1 A declaration of a non-TU-local entity that is an exposure (6.6) is deprecated. [Note: Such a declaration in an importable module unit is ill-formed. — end note]


    Takeaway

    Per the Standard, the foos have external linkage. But notwithstanding the Standard, compilers give them internal linkage, with more or less explicit warnings. Giving them external linkage would cost ODR exposure as per my example while also being generally worthless for symbol resolution purposes: it's all downside.

    It is an unsavoury state of affairs for compilers to be playing the He doesn't really mean it role to the Standard's fumbled position on the linkage of lambda-compounded types. In the new TU-local language, it arises from the admission of classes, including lambda types, as non-type template parameters, whereby a TU-local lambda-type can be exposed by that of a non-TU-local template-instantiating type. Compilers are in an invidious position while this remains legal, because they must pick one of external or internal linkage to ascribe to the emitted symbol of the mashed TU-local + non-TU-local function or object, and internal linkage is clearly the least worst choice. Modules have put a spotlight on the loophole, and in module interface code the Standard has closed it. Elsewhere, as in your code, it is not closed, but tagged for closure.