6

Using GCC 11,

gcc --version
gcc (GCC) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

I'm building a library libsimplemath.a, based on two objects (add.o and sub.o), sub.o has an external dependency on undef_symb:

// add.cpp
int add(int a, int b) { return a + b; }

// sub.cpp
#include <string>
void undef_symb(void);
int sub(int a, int b)
{
  std::string s;
  undef_symb();
  return a - b;
}
g++ -O0 -std=c++20 -o sub.o -c sub.cpp
g++ -O0 -std=c++20 -o add.o -c add.cpp
ar rc libsimplemath.a add.o sub.o

Let's say one of the libsimplemath.a user has a main only depending on add interface:

// main.cpp
#include <string>
int add(int, int);
int main()
{
  std::string s = "Hello";
  return add(0, 0);
}

When the code is compiled using c++20 flag, everything works fine:

g++ -O0 -std=c++20 -o main.o -c main.cpp
g++ -o main main.o -L. -lsimplemath

No problem.

However when the code is compiled using c++17 flag (and this is the current user flow), it fails at link time:

g++ -O0 -std=c++17 -o main.o -c main.cpp
g++ -o main main.o -L. -lsimplemath                       
/arm/tools/gnu/binutils/2.32/rhe7-x86_64/bin/ld: ./libsimplemath.a(sub.o): in function `sub(int, int)':
sub.cpp:(.text+0x1c): undefined reference to `undef_symb()'
collect2: error: ld returned 1 exit status

Here's a CMake project @ compiler explorer that reproduces the problem.

For some reasons sub.o is also brought by linker and, as it contains an undefined symbol, linking is failing.

To know why sub.o is taken into account linker map file can be looked at (-Wl,-Map=main.map linker option):

Archive member included to satisfy reference by file (symbol)

./libsimplemath.a(add.o)      main.o (add(int, int))
./libsimplemath.a(sub.o)      main.o (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string())

basic_string stl symbol is resolved in sub.o.

By looking at nm output we can see that basic_string constructor is not defined in main.o:

nm -aC main.o | grep basic_string
                 U std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string()

It is defined in sub.o:

nm -aC sub.o | grep basic_string\(
0000000000000000 W std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string()

It explains why sub.o is brought by linker, and the fail.

The fact that basic_string symbols are defined or not in the object file depends on the -std=c++XX flag used. Compiling main.cpp with -std=c++20:

 nm -aC main.o | grep basic_string\(
0000000000000000 W std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string()

The symbol is defined, no need to resolve it elsewhere, thus sub.o is not brought and linking works.

The reason why basic_string is defined or not seems to come from the fact that it's defined as extern or not: in include/c++/11.3.0/bits/basic_string.tcc

  // Inhibit implicit instantiations for required instantiations,                                                                                                                                          
  // which are defined via explicit instantiations elsewhere.                                                                                                                                              
#if _GLIBCXX_EXTERN_TEMPLATE
  // The explicit instantiation definitions in src/c++11/string-inst.cc and                                                                                                                                
  // src/c++17/string-inst.cc only instantiate the members required for C++17                                                                                                                              
  // and earlier standards (so not C++20's starts_with and ends_with).                                                                                                                                     
  // Suppress the explicit instantiation declarations for C++20, so C++20                                                                                                                                  
  // code will implicitly instantiate std::string and std::wstring as needed.                                                                                                                              
# if __cplusplus <= 201703L && _GLIBCXX_EXTERN_TEMPLATE > 0
  extern template class basic_string<char>;

For std=c++17 the template is extern and thus basic_string is not defined in main.o.

So this use-case seems to break the interoperability between different standard (here c++17 and c++20). Is it expected?

If there is no problem here then it means every single dependency must be provided at link time? Even the ones the user (main writer) doesn't care of? (I didn't find resources claiming it)

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
Romain B
  • 61
  • 4
  • I tried what you proposed, it didn't make it work. As you noticed it is not failing on more recent gcc (I tested on gcc 9.3.0). From my experiments it is reprocucable on gcc 7.3.0 and gcc 8.3.0 – Romain B Nov 20 '20 at 14:34
  • I updated the case with gcc11, outdating the above comments – Romain B Aug 22 '22 at 10:08
  • Hi @TedLyngmo, thanks for having a look, the issue comes when you are compiling the static lib with -std=c++20 and main.cpp with -std=c++17. Did you do that? I might be wrong but it looks like you are compiling everything with -std=c++17 in your case. – Romain B Aug 23 '22 at 12:45
  • Aha, I missed that important detail. I updated the CMake project so it reproduces your problem and put that in your question. – Ted Lyngmo Aug 23 '22 at 13:30
  • This looks like a bug to me. – n. m. could be an AI Aug 23 '22 at 14:28
  • Romain: You're welcome. @n.1.8e9-where's-my-sharem. It feels like ABI breakage. According to [this](https://stackoverflow.com/questions/46746878/is-it-safe-to-link-c17-c14-and-c11-objects), it _should_ work - if this kind if linking isn't in fact IFNDR. – Ted Lyngmo Aug 23 '22 at 15:12
  • @TedLyngmo It is still entirely safe to link together C++11, C++15 and C++17 objects. The problem is that the linker stumbles upon a wrong object to link. If you pass main.o and add.o to the linker (instead of libsimplemath) it works. If you add sub.o it stops working (IFNDR but the implementations usually do give a diagnostic). Should the linker bring in sub.o automatically? The standard doesn't attempt to answer this question. It's a QoA issue. – n. m. could be an AI Aug 23 '22 at 16:26
  • @n.1.8e9-where's-my-sharem. You're right. I made the `add` function return a `std::string` instead and received that in `main`. Even if compiled with different standards it looks like it [works fine](https://godbolt.org/z/j8e58bqYo). – Ted Lyngmo Aug 23 '22 at 16:36

1 Answers1

0

extern template class ... is a totally normal C++ declaration that can appear in user code, regardless of the C++ standard chosen. You are not supposed to instantiate things from the standard library yourself (whether it's legal or not), but the same exact thing happens with any user-defined class template. You can build a virtually identical example using no standard library and no -std= options. Just make a template, add extern template class ... in one file, and don't add it to another file.

No standard and no ABI can protect you from things like that. You always need to be careful about which objects you link into your program.

Of course it is still a problem in practice and IMHO a bug in the libstdc++ (it should not make easy things hard), but once you know what's going on, there is a simple workaround.

Compile (with -std=c++20 -O0) an object that contains this:

// for the benefit of C++17 users
// who compile their libraries with -O0
#include <string>
std::string a;

and link it to your program before the linker gets a chance to see sub.o. For example, put it first in libsimplemath.a.

You still can interoperate between C++17 and C++20 just fine. No need to provide every dependency at link time (it would be a horrible thing to do).

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243