Our website uses cookies to enhance your browsing experience.
Accept
to the top
close form

Fill out the form in 2 simple steps below:

Your contact information:

Step 1
Congratulations! This is your promo code!

Desired license type:

Step 2
Team license
Enterprise license
** By clicking this button you agree to our Privacy Policy statement
close form
Request our prices
New License
License Renewal
--Select currency--
USD
EUR
* By clicking this button you agree to our Privacy Policy statement

close form
Free PVS‑Studio license for Microsoft MVP specialists
* By clicking this button you agree to our Privacy Policy statement

close form
To get the licence for your open-source project, please fill out this form
* By clicking this button you agree to our Privacy Policy statement

close form
I am interested to try it on the platforms:
* By clicking this button you agree to our Privacy Policy statement

close form
check circle
Message submitted.

Your message has been sent. We will email you at


If you haven't received our response, please do the following:
check your Spam/Junk folder and click the "Not Spam" button for our message.
This way, you won't miss messages from our team in the future.

>
>
>
C++20: linker surprised by four lines o…

C++20: linker surprised by four lines of code

Dec 16 2021
Author:

Imagine that you are a student learning modern C++ features. And you have to complete a task concerning concepts/constraints. The teacher, of course, knows the proper way to do it – but you don't. You've already written spaghetti code that does not work. (And you keep adding more and more overloads and templates specializations to solve escalating compiler claims).

We published and translated this article with the copyright holder's permission. The author is Nikolay Merkin. The article was originally published on Habr.

Now imagine that you are a teacher who's watching this spaghetti code and wants to help the student. You start simplifying the code, and even comment on unit tests fragments to make this work somehow... But nothing's changed – the code doesn't work. Moreover, the code outputs different results or is not built at all, depending on the order of unit tests. Undefined behavior is hidden somewhere. But where is it?

0899_C++20_surprise_linker/image1.png

First, the teacher (I) minimized the code as follows: https://gcc.godbolt.org/z/TaMTWqc1T

// suppose we have concept Ptr and concept Vec
template<class T> concept Ptr = requires(T t) { *t; };
template<class T> concept Vec = requires(T t) { t.begin(); t[0]; };

// and three overloaded functions recursively defined through each other
template<class T> void f(T t) {  // (1)
  std::cout << "general case " << __PRETTY_FUNCTION__ << std::endl;
}
template<Ptr T> void f(T t) {  // (2)
  std::cout << "pointer to ";
  f(*t);  // suppose the pointer is not null
}
template<Vec T> void f(T t) {  // (3)
  std::cout << "vector of ";
  f(t[0]);  // suppose the vector is not empty
}

// and a test set (in different files)
int main() {
  std::vector<int> v = {1};

  // test A
  f(v);
  // or test B
  f(&v);
  // or test C
  f(&v);
  f(v);
  // or test D
  f(v);
  f(&v);
}

We expect that

  • f(v) outputs "vector of general case void f(T) [T=int]"
  • f(&v) outputs "pointer to vector of general case void f(T) [T=int]"

But instead, we get

  • A: "vector of general case void f(T) [T=int]"
  • B: "pointer of general case void f(T) [T=std::vector<int>]" — ?
  • C: clang outputs "pointer to general case void foo(T) [T = std::vector<int>]" — as in B. "general case void foo(T) [T = std::vector<int>]", — not as in A! gcc — issues linker error
  • D: clang and gcc issue linker error

What's wrong with this code?!

Two things are wrong here. The first is that we see only (1) and (2) declarations of function (2), so the result of pointer dereference is called as (1).

Also, we can perfectly reproduce it without concepts and templates: https://gcc.godbolt.org/z/47qhYv6q4

void f(int x)    { std::cout << "int" << std::endl; }
void g(char* p)  { std::cout << "char* -> "; f(*p); }  // f(int)
void f(char x)   { std::cout << "char" << std::endl; }
void g(char** p) { std::cout << "char** -> "; f(**p); }  // f(char)
int main() {
  char x;
  char* p = &x;
  f(x);  // char
  g(p);  // char* -> int
  g(&p); // char** -> char
}

Unlike inline member functions in the class, where all members see all declarations — a free function sees only what is higher in the file.

That's why, we have to write declarations and definitions separately for mutually recursive functions.

Ok, we figured it out. Let's get back to templates. Why did we get something similar to an ODR violation in tests C and D?

If we rewrite the code as follows:

template<class T> void f(T t) {.....}
template<class T> void f(T t) requires Ptr<T> {.....}
template<class T> void f(T t) requires Vec<T> {.....}

nothing changes. This is just another way to write the code. We can write it in different ways to meet the concept requirements.

However, if we use good old SFINAE https://gcc.godbolt.org/z/4sar6W6Kq

// add a second argument char or int - to resolve ambiguity
template<class T, class = void> void f(T t, char) {.....}
template<class T> auto f(T t, int) -> std::enable_if_t<Ptr<T>, void> {.....}
template<class T> auto f(T t, int) -> std::enable_if_t<Vec<T>, void> {.....}
..... f(v, 0) .....
..... f(&v, 0) .....

or an old-school argument type matching, https://gcc.godbolt.org/z/PsdhsG6Wr

template<class T> void f(T t) {.....}
template<class T> void f(T* t) {.....}
template<class T> void f(std::vector<T> t) {.....}

then everything works. Not the way we wanted (recursion is still broken because of scope rules), but as we expected (the vector from f(T*) is seen as "general case", from main – as "vector").

What else is about concepts/constraints?

Thanks to RSDN.org, we brainstormed the code and found the way to optimize it!

Only 4 lines:

template<class T> void f() {}
void g() { f<int>(); }
template<class T> void f() requires true {}
void h() { f<int>(); }

It's better to use a constraint function than a function without constraints. Therefore, according to the scope rules, g() has the only one option to choose, but h() has two options and chooses the second one.

And this code generates an incorrect object file! It has two functions with the same mangled names.

It turns out that modern compilers (clang ≤ 12.0, gcc ≤ 12.0) do not know how to consider requires in name mangling. As it was with old and not so smart MSVC6 that did not take into account the template parameters if they did not affect the function type...

Considering the developers' replies, they do not know how and do not want to fix it. Here's what they say: "If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required" (however, ill-formed means "not compiled", but not "compiled randomly"...)

The issue is known since 2017, but there is no progress yet.

So, take it or leave it. Don't forget to declare mutually recursive functions before declarations. And if you notice strange linker errors, then at least now you know why they arise. (But if the compiler randomly inlines — bad luck!).



Comments (0)

Next comments next comments
close comment form