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?
(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 foo
s 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 foo
s.
Now let's compile main.cpp
, the one where the foo
s 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 foo
s 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 foo
s:
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 foo
s 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 foo
s 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 foo
s 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 foo
s 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 foo
s,
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 foo
s the compilers
appear prima facie to be foul the Standard. @YurkoFlisk argues that:
a) The ODR is not jeopardised by the foo
s having external linkage, because lambda
types are unique per expr.prim.lambda.closure, making ODR violations between any two definitions
having a lambda type or a lambda-compounded type impossible.
b) The Standard isn't really violated anyway by the compilers emitting local symbols, because:
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 foo
s are actually
given external linkage.
For b), we do not even need to have the implementation (the object code) and observe that the foo
s are emitted as local symbols to infer - rightly or wrongly - that the foo
s 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 foo
s internal linkage.
So I suggest that all the compilers are indeed foul of the Standard
when they compile the definitions of the foo
s 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
- a type, function, variable, or template that
- has a name with internal linkage, or
- does not have a name with linkage and is declared, or introduced by a lambda expression, within the definition of a TU-local entity,
- 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,
- a specialization of a TU-local template,
- a specialization of a template with any TU-local template argument, or
- 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 foo
s 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 foo
s 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 foo
s 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.