Search code examples
c++return-typeforward-declaration

Incorrect return type allowed for forward declared function: Why is there no linker error here?


$ g++ --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 12.0.0 (clang-1200.0.32.29)
Target: x86_64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

a.cc

#include<iostream>

using namespace std;

static int x = 5053;

void f2();

int main() {
  cout << "a: " << x << endl;
  f2();
  return 0;
}

b.cc

#include<iostream>

using namespace std;

static int x = 4921;

string f2() {
  cout << "b: " << x << endl;
  return "";
}

Output

$ g++ --std=c++17 a.cc b.cc && ./a.out
a: 5053
b: 4921

Why was I able to forward declare string f2(); from b.cc as void f2(); in a.cc?

Any references to cppreference or spec that allows this would be appreciated.


Solution

  • We don't always get a linker error because the C++ standard does not require a linker error or any other error in this case, and the implementations most of the time do not go the extra mile.

    The standard does not require an error because the common linker technology does not allow the implementation to detect such errors.

    Stroustrup decided to leave the return type out of C++ name mangling, so that names of two functions that differ only by their return type mangle to the same symbol name. This makes some ODR violation errors silently go undetected. Making the return type participate in the mangling would make other ODR violation errors go undetected.

    There is no way to make all ODR-violation errors detectable with existing commonly available linkers. So the standard essentially permits the implementation to do what the original Stroustrup's compiler did: leave the return type out of the name mangling.

    The compiler you use does just that. So it is something like _Z2f2v in the object file. You can see that by typing nm a.o | grep f2 and nm b.o | grep f2, or looking at the assembly output on the compiler explorer (use clang, add -stdlib=libc++ to the options and deselect "demangle symbols" in the menu). There is no indication anywhere that the function is supposed to return any specific type.

    So why does gcc detect this error then?

    That's because sometimes gcc does include the return type in the name mangling, and your program just happens to hit that special case. It has to do with the big great ABI breakage of C++11. gcc and libstdc++ had to change the layout of some standard library classes, notably std::string (also std::list but hey who uses that?) So the gcc authors did a clever thing to maintain backwards compatibility: they changed the mangling of everything that involves these classes --- and this time in the return type too. This way, old ABI code cannot link with new ABI code without errors. The names won't match.

    You can confirm that by invoking nm on objects compiled with g++ or going to the compiler explorer again. You will see that an object than mentions void f2() still has something like _Z2f2v in it, but an object that mentions string f2() has something like _Z2f2B5cxx11v (note cxx11 in the name -- this way we know we are dealing with the new post-C++11 ABI). All is well, but if you use both functions in the same program (in different translation units), the compiler won't detect it, which is the other kind of undetectable ODR violation (still allowed by the standard).

    This does not happen with other types, so if you change f2 to return say std::vector<char>, you will not get a linker error with either compiler.