Jan Špaček
Jan Špaček

Reputation: 1108

Automatic constructor in explicitly instantiated class template

I have a template<bool VAR> struct Obj template declared in a header file (obj.h) with explicit automatic move constructor (= default).

// obj.h
#pragma once
#include <vector>

template<bool VAR>
struct Obj {
  std::vector<int> member;
  Obj(int m): member(m) { }
  Obj(Obj&&) = default;
  int member_fun() const;
};

extern template struct Obj<false>;
extern template struct Obj<true>;

The member function of the template is defined in another file (obj.cpp) with explicit instantiation of the template:

// obj.cpp
#include "obj.h"

template<bool VAR>
int Obj<VAR>::member_fun() const {
  return 42;
}

template struct Obj<false>;
template struct Obj<true>;

This template is then used from the main file (main.cpp):

// main.cpp
#include <utility>
#include "obj.h"

int main() {
  Obj<true> o1(20);
  Obj<true> o2(std::move(o1));
  return o2.member_fun();
}

The .cpps are then compiled and linked together with the following Makefile:

#CXX=clang++
CXX=g++
CXXFLAGS=-Wall -Wextra -std=c++14

a.out: obj.o main.o
    $(CXX) $(CXXFLAGS) $^ -o a.out

obj.o: obj.cpp obj.h
    $(CXX) $(CXXFLAGS) -c $< -o $@
main.o: main.cpp obj.h
    $(CXX) $(CXXFLAGS) -c $< -o $@

However, I get a linker error: undefined reference to 'Obj<true>::Obj(Obj<true>&&)' -- the compiler apparently did not instantiate the constructor.

(I received this error with Clang in a project that compiled fine with GCC, so I thought this was a Clang bug. However, when I reduced the problem to this simple case, both GCC 5.4.0 and Clang 3.8.0 produce the same results.)

Upvotes: 9

Views: 988

Answers (3)

Dave Dopson
Dave Dopson

Reputation: 42714

It's possible that you are hitting a compiler bug.

See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60796.

I'm hitting similar behavior on CLang, but can't find a bug report.

Upvotes: 0

rustyx
rustyx

Reputation: 85382

Interesting. I think your code is correct, because:

Your defaulted move-constructor is implicitly inline because of:

[dcl.fct.def.default]/5:

... A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted.

And [class.mfct]/1:

A member function may be defined ([dcl.fct.def]) in its class definition, in which case it is an inline member function ([dcl.fct.spec])

And thus is exempt from explicit template instantiation according to [temp.explicit]/10 (emphasis mine):

Except for inline functions and variables, declarations with types deduced from their initializer or return value ([dcl.spec.auto]), const variables of literal types, variables of reference types, and class template specializations, explicit instantiation declarations have the effect of suppressing the implicit instantiation of the entity to which they refer. [ Note: The intent is that an inline function that is the subject of an explicit instantiation declaration will still be implicitly instantiated when odr-used ([basic.def.odr]) so that the body can be considered for inlining, but that no out-of-line copy of the inline function would be generated in the translation unit. — end note ]

In fact, if you try any optimization mode other than -O0, the problem disappears.

-O0 is a special mode in which inlined functions are not inlined. But it shouldn't matter, the compiler must in that case generate the inline defaulted move-constructor like it does with the other constructor.

So to me this looks like a compiler bug. Also look at LLVM#22763 and GCC#60796.

I see at least 2 possible workarounds:

Solution 1

Do not use extern template ... for now (compilation times will suffer but otherwise no big deal).

Solution 2

Trick the compiler into generating the suppressed constructor in obj.cpp

template<> Obj<true>::Obj(Obj&&) noexcept = default;

This one will only be used in -O0 mode. In production code the inlined version will be used instead.

Upvotes: 5

JVApen
JVApen

Reputation: 11317

There doesn't exist a lot of information about this topic on the internet. Though looking at some sources, I would conclude the following:

extern template should prevent implicit instantiations, though in all examples, the explicit instantiation doesn't have this extern template definition.

From what I can read, especially on the proposal and the GCC mailing list (see references below), the extern template doesn't prevent implicit instantiations, though ALL instantiations of the template. This would include your explicit instantiation.

If an entity is the subject of both an explicit instantiation declaration and an explicit instantiation definition in the same translation unit, the definition shall follow the declaration. - John Spicer on GCC mailing list

From this, I would conclude that you should remove the extern template in the translation unit where you want the explicit instantiation.

References:

Upvotes: 2

Related Questions