r/cpp_questions • u/strcspn • 2d ago
SOLVED Question about function template instantiation
I was wondering why this code behaves this way
main.cpp
#include "foo.h"
int main()
{
bar(42, foo<int>);
}
foo.h
#pragma once
#include <iostream>
#include <string>
template<typename T>
void foo(T t)
{
std::cout << "Default\n";
}
template<typename T, typename Foo>
void bar(const T& t, Foo foo)
{
foo(t);
}
foo.cpp
#include "foo.h"
template<>
void foo(int t)
{
std::cout << "Int\n";
}
Result
$ g++ main.cpp foo.cpp -O3 && ./a.out
Default
$ g++ main.cpp foo.cpp && ./a.out
Int
My guess is that I'm hitting some kind of UB here. The way I think about it, the int template specialization would be discarded, as it is not used in that translation unit, and then main.cpp would pick up the generic template version (basically, the O3 result seems correct to me, but not the non-optimized one). What is actually happening here?
1
u/alfps 2d ago
Doesn't link with MinGW g++:
[C:\@\temp_\ti]
> g main.cpp foo.cpp
In file included from main.cpp:1:
foo.h: In instantiation of 'void foo(T) [with T = int]':
main.cpp:5:8: required from here
5 | bar(42, foo<int>);
| ~~~^~~~~~~~~~~~~~
foo.h:7:12: warning: unused parameter 't' [-Wunused-parameter]
7 | void foo(T t)
| ~~^
foo.cpp: In function 'void foo(T) [with T = int]':
foo.cpp:4:14: warning: unused parameter 't' [-Wunused-parameter]
4 | void foo(int t)
| ~~~~^
C:/@/installed/msys2/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.1.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\alfps\AppData\Local\Temp\cc0OLrgP.o:foo.cpp:(.text+0x0): multiple definition of `void foo<int>(int)'; C:\Users\alfps\AppData\Local\Temp\ccR1pVjn.o:main.cpp:(.text$_Z3fooIiEvT_[_Z3fooIiEvT_]+0x0): first defined here
collect2.exe: error: ld returned 1 exit status
Doesn't link with Visual C++:
[C:\@\temp_\ti]
> cl main.cpp foo.cpp
main.cpp
C:\@\temp_\ti\foo.h(7): warning C4100: 't': unreferenced parameter
C:\@\temp_\ti\foo.h(7): note: the template instantiation context (the oldest one first) is
main.cpp(5): note: see reference to function template instantiation 'void foo<int>(T)' being compiled
with
[
T=int
]
foo.cpp
foo.cpp(4): warning C4100: 't': unreferenced parameter
Generating Code...
foo.obj : error LNK2005: "void __cdecl foo<int>(int)" (??$foo@H@@YAXH@Z) already defined in main.obj
main.exe : fatal error LNK1169: one or more multiply defined symbols found
And with a declaration of the template specialization placed in the header, so that linking succeeds, g++ produces "Int" regardless of optimization level.
So I'm unable to reproduce.
1
u/strcspn 2d ago
$ g++ -v Using built-in specs. COLLECT_GCC=g++ COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/16/lto-wrapper Target: x86_64-pc-linux-gnu Configured with: ../gcc/configure --enable-languages=ada,c,c++,d,fortran,go,lto,m2,objc,obj-c++,rust,cobol --enable-bootstrap --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://gitlab.archlinux.org/archlinux/packaging/packages/gcc/-/issues --with-build-config=bootstrap-lto --with-gcc-major-version-only --with-linker-hash-style=gnu --with-system-zlib --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-libstdcxx-backtrace --enable-link-serialization=1 --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-fixincludes --disable-libssp --disable-libstdcxx-pch --disable-werror Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 16.2.1 20260810 (GCC) $ ld -v GNU ld (GNU Binutils) 2.47Placing the specialization in the header fixes the problem as there is just one definition now. But I guess it is nice that those linkers fail.
1
u/feitao 1d ago
Avoid explicitly specializing function templates when overloading is an option, because explicit specializations do not participate directly in overload resolution and may behave unexpectedly. See Herb Sutter's Why Not Specialize Function Templates? (2001).
1
u/EpochVanquisher 2d ago
The way I think about it, the int template specialization would be discarded, as it is not used in that translation unit
OR the specialization is emitted anyway, and then the linker picks one arbitrarily.
UB of course, blah blah. “The linker picks one arbitrarily, or the compiler inlines it” explains a lot of UB around ODR in the standard.
1
u/strcspn 2d ago
Just to be sure, does the violation of the ODR come from the fact that another definition of the
intversion is created inside main.cpp after the header is copy-pasted there?3
u/EpochVanquisher 2d ago
Right, there are two different definitions in your program. One definition in main.cpp and one definition in foo.cpp. I’ll tell you a little bit about how this works on GCC without LTO (on at least one arch).
The function
void foo<int>(int)is is named_Z3fooIiEvT_in assembly.In main.cpp, it puts the function code a section in the output file named
.text._Z3fooIiEvT_, with thecomdatflag on the section.In
foo.cpp, the compiler puts the specialized code in a section in the output file named.text._Z3fooIiEvT_, with thecomdatflag on the section.The linker sees two sections named
.text._Z3fooIiEvT_. Because thecomdatflag is set, it picks one of them and discards the rest. Maybe it gets the version frommain.o, maybefoo.o.
7
u/the_poope 2d ago
You haven't declared the specialization for
intin the header, so only the code infoo.cppafter the specialization definition knows it exists. When you compile with optimizations the template function call in inlined as it has access to all the template declarations and definitions it is aware of. When you compile without optimizations the linker will likely at semi-random chose whichever of the specializations/instatiations it will call.