Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

  • Obj<true>::member_fun() is defined and the program indeed links successfully if I remove the reference to the move constructor from main.cpp.
  • If I remove extern template from the header, the program compiles.
  • If I use int instead of std::vector<int> for the type of member, the program also compiles.
  • cppreference.com claims that "the compiler will declare a move constructor as a non-explicit inline public member of its class". However, the manually defined Obj(int) constructor is also inline, but it is correctly instantiated.

(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.)

like image 520
Jan Špaček Avatar asked Aug 13 '16 06:08

Jan Špaček


People also ask

What happens when a class template is instantiated?

Template instantiation involves generating a concrete class or function (instance) for a particular combination of template arguments. For example, the compiler generates a class for Array<int> and a different class for Array<double>.

Can template class have constructor?

We can use this technique in our template code to create default constructors. For any generic type, T, we can explicitly call its default constructor. Our Measurement template can now be constructed with any type that has a default constructor.

What is the instantiation of the class template?

The act of creating a new definition of a function, class, or member of a class from a template declaration and one or more template arguments is called template instantiation.

How do I force a template instantiation?

To instantiate a template function explicitly, follow the template keyword by a declaration (not definition) for the function, with the function identifier followed by the template arguments. template float twice<float>(float original); Template arguments may be omitted when the compiler can infer them.


1 Answers

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.

like image 167
rustyx Avatar answered Oct 23 '22 09:10

rustyx