Bug 89087 - Dllexport for explicit template instantiation with nested classes loses nested class
Summary: Dllexport for explicit template instantiation with nested classes loses neste...
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 8.2.0
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
Keywords: link-failure
Depends on:
Reported: 2019-01-28 09:23 UTC by Martin Storsjö
Modified: 2019-04-26 19:51 UTC (History)
1 user (show)

See Also:
Target: *-*-mingw*
Known to work:
Known to fail:
Last reconfirmed:

Sample code showing the issue (781 bytes, application/zip)
2019-01-28 09:23 UTC, Martin Storsjö

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Storsjö 2019-01-28 09:23:26 UTC
Created attachment 45537 [details]
Sample code showing the issue

When an explicit template instantiation of a template class with a nested class is declared with the dllexport attribute, only members from the outer class actually gets the embedded export directive.

A caller that sees the explicit template instantiation declaration won't emit those symbols but produce undefined references to them (both for the outer and inner class), relying on the template instantiation in a different translation unit.

If relying on the dllexport attribute for exporting the relevant symbols, only the outer class' members are exported, and linking to the dll fais.

To showcase the problem:

template <class T> struct outer {
  void f();
  struct inner {
    void f();

template <class T> void outer<T>::f() {}
template <class T> void outer<T>::inner::f() {}

extern template class
#elif defined(DLLIMPORT)

#include "header.h"

template class outer<char>;

#include "header.h"

int main(int argc, char* argv[]) {
  outer<char> a;
  outer<char>::inner b;
  return 0;

Building this fails in this way:
$ make
x86_64-w64-mingw32-g++    -c -o caller.o caller.cpp
x86_64-w64-mingw32-g++    -c -o lib.o lib.cpp
x86_64-w64-mingw32-g++ -shared -o lib.dll lib.o -Wl,--out-implib,liblib.dll.a
x86_64-w64-mingw32-g++ -o caller.exe caller.o -L. -llib
caller.o:caller.cpp:(.text+0x28): undefined reference to `outer<char>::inner::f()'
collect2: error: ld returned 1 exit status
Makefile:5: recipe for target 'caller.exe' failed
make: *** [caller.exe] Error 1

The template instantiation in lib.cpp does get both outer and inner function definitions:
$ x86_64-w64-mingw32-nm lib.o
0000000000000000 T _ZN5outerIcE1fEv
0000000000000000 T _ZN5outerIcE5inner1fEv

And the caller gets undefined references to the same:
$ x86_64-w64-mingw32-nm caller.o 
0000000000000000 T main
                 U _ZN5outerIcE1fEv
                 U _ZN5outerIcE5inner1fEv

But only the outer function actually ended up exported from the DLL:
$ x86_64-w64-mingw32-objdump -s lib.o
Contents of section .drectve:
 0000 202d6578 706f7274 3a225f5a 4e356f75   -export:"_ZN5ou
 0010 74657249 63453166 45762200           terIcE1fEv".    

If the DLL is linked with -Wl,--export-all-symbols, both functions are exported from the DLL and linking succeeds.

This is contrary to MSVC (which admittedly has got an entirely different C++ ABI). In MSVC, the caller emits the inner class' methods despite the explicit template instantiation (both when the template instantiation was marked dllimport, but also if dllimport is omitted):

With dllimport:
$ cl -nologo -c caller.cpp 
$ x86_64-w64-mingw32-nm caller.obj 
0000000000000000 T ?f@inner@?$outer@D@@QEAAXXZ
                 U __imp_?f@?$outer@D@@QEAAXXZ
0000000000000000 T main

Without dllimport:
$ cat caller.cpp | sed 's/^#def.*//' > caller-nodllimport.cpp
$ ~/msvc2017/bin64/cl -nologo -c caller-nodllimport.cpp 
$ x86_64-w64-mingw32-nm caller-nodllimport.obj 
0000000000000000 T ?f@inner@?$outer@D@@QEAAXXZ
                 U ?f@?$outer@D@@QEAAXXZ
0000000000000000 T main

To solve this (short of requiring using -Wl,--export-all-symbols on any library that uses explicit template instantiation with nested classes), the dllexport either needs to cover the nested class, or an explicit template instantiation should only be considered to cover the outer class.
Comment 1 Martin Storsjö 2019-04-26 19:51:33 UTC
FWIW, Clang (when operating in MinGW mode, where it tries to follow what GCC does) also had the same issue. There this issue was fixed by emitting definitions for nested classes even if a template instantiation has been declared, like this: https://github.com/llvm-project/clang/commit/7331c3301af9628719664c9a4feea576df3994a9