Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling a function template before and after a more constrained version is defined gives weird results

My coworkers showed me following example today:

Run on gcc.godbolt.org

#include <concepts>
#include <iostream>

template <typename T>
void foo(T)
{
    std::cout << "1\n";
}

template <typename T>
void bar(T value)
{
    foo(value);
}

void foo(std::same_as<int> auto)
{
    std::cout << "2\n";
}

Here, bar(42); and foo(42); print 1 and 2 respectively, if you only call one of them.

If you call both (in this order), then:

  • Clang prints 1 1.
  • GCC emits a linker error, complaining about multiple definitions of foo.
  • MSVC prints 2 2 in release builds and emits a similar linker error in debug builds (could be affected by incremental linking, didn't investigate).

What's going on here? The code looks well-formed to me, but maybe I'm wrong?

like image 679
HolyBlackCat Avatar asked Jun 10 '21 18:06

HolyBlackCat


1 Answers

Cool. Every compiler is wrong.

Within bar, the call to foo(value) only has the unconstrained foo<T> visible in scope. So when we call foo(value), the only possible candidates are (1) that one (2) whatever argument-dependent lookup finds. Since T=int in our example, and int has no associated namespaces, (2) is an empty set. As a result, when bar(42) calls foo(42), that's the foo that is the unconstrained template, which should print 1.

On the other hand, within main, foo(42) has two different overloads to consider: the constrained one and the unconstrained one. The constrained one is viable, and is more constrained than the unconstrained one, so that's preferred. So from within main(), foo(42) should call the constrained foo(same_as<int> auto) which should print 2.

To summarize:

  • Clang gets this wrong because it apparently caches the foo<int> call and that's incorrect - the other foo overload isn't a specialization, it's an overload, it needs to be considered separately.

  • gcc gets this right in that the two different foo calls call two different foo function templates, but gets this wrong in that it mangles both the same so that we end up with a linker error. This is Itanium ABI #24.

  • MSVC gets this wrong in that argument-dependent lookup within bar for foo(value) finds the later-declared foo.

More fun is if you change the functions to be constexpr int instead of void, which lets you verify this behavior at compile time... as in:

#include <concepts>
#include <iostream>

template <typename T>
constexpr int foo(T)
{
    return 1;
}

template <typename T>
constexpr int bar(T value)
{
    return foo(value);
}

constexpr int foo(std::same_as<int> auto)
{
    return 2;
}

static_assert(bar(42) == 1);
static_assert(foo(42) == 2);

int main()
{
    std::cout << bar(42) << '\n';
    std::cout << foo(42) << '\n';
}

Then clang compiles (i.e. it does correctly give you that bar(42) == 1 and foo(42) == 2 from that spot) but then prints 2 twice anyway.

While gcc still compiles, just having the same linker error because it mangles both function templates the same.

like image 81
Barry Avatar answered Oct 19 '22 08:10

Barry