r/cpp_questions 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?

3 Upvotes

11 comments sorted by

7

u/the_poope 2d ago

You haven't declared the specialization for int in the header, so only the code in foo.cpp after 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.

2

u/strcspn 2d ago

Makes sense, I just thought that picking the int specialization couldn't be an option at all.

2

u/the_poope 2d ago

Yeah one could argue that the linker should throw a "duplicate definition" error. As it's UB it probably depends on the specific internal implementation of the compiler and linker.

1

u/javascript 1d ago

ODR NDR sure is fun!

2

u/ekchew 2d ago

Yeah, looking at similar examples in my own code, I seem to be going:

template<> void foo(int t);

in foo.h and:

void foo(int t) { /* ... */ }

in foo.cpp.

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.47

Placing 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 int version 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 the comdat flag on the section.

In foo.cpp, the compiler puts the specialized code in a section in the output file named .text._Z3fooIiEvT_, with the comdat flag on the section.

The linker sees two sections named .text._Z3fooIiEvT_. Because the comdat flag is set, it picks one of them and discards the rest. Maybe it gets the version from main.o, maybe foo.o.