Home > other >  Inheriting from template instanciated with incomplete type
Inheriting from template instanciated with incomplete type

Time:05-12

I have a struct template like the following:

// S.h
#pragma once

#include <vector>

template <typename T>
struct S
{
    std::vector<T*> ts;

    virtual ~S() { for (auto* t : ts) t->foo(); } 
    void attach(T& t) { this->ts.push_back(&t); }
};

Then, I inherit a non-template struct, ConcreteS, from S<A>; struct A being incomplete at this point, as I only forward declare it in ConcreteS.h:

// ConcreteS.h
#pragma once

#include "S.h"

struct A;
struct ConcreteS : public S<A>
// struct A incomplete here ^
{
    ConcreteS(); 
    ~ConcreteS() override; 
};

I include A.h in ConcreteS.cpp to make the definition of struct A visible to the implementation of ConcreteS's destructor:

// ConcreteS.cpp
#include "ConcreteS.h"
#include "A.h"

#include <cstdio>

ConcreteS::ConcreteS() { std::puts("ConcreteS()"); }
ConcreteS::~ConcreteS() { std::puts("~ConcreteS()"); }

Finally, I instanciate ConcreteS in function main:

// main.cpp
#include "ConcreteS.h"

int main()
{
    ConcreteS concreteS{};
}

The above code compiles (and runs) fine with:

  • GCC 11.3.0;
  • Clang 14.0.0.

The output is:

ConcreteS()
~ConcreteS()

But fails to compile on:

  • Visual Studio 2019 (compiler: MSVC 19.29.30133.0);
  • Visual Studio 2013 (compiler: MSVC 18.0.40629.0).

Here you are the error message from VS13 (the one from VS19 is similar):

Microsoft (R) Build Engine version 12.0.40629.0
[Microsoft .NET Framework, version 4.0.30319.42000]
Copyright (C) Microsoft Corporation. All rights reserved.

  Checking Build System
  Building Custom Rule <proj_path>/CMakeLists.txt
cl : Command line warning D9002: ignoring unknown option '/permissive-' [<proj_path>\build_vs13\tmp.vcxproj]
  A.cpp
  ConcreteS.cpp
  main.cpp
<proj_path>\S.h(10): error C2027: use of undefined type 'A' [<proj_path>\build_vs13\tmp.vcxproj]
          <proj_path>\ConcreteS.h(5) : see declaration of 'A'
          <proj_path>\S.h(10) : while compiling class template member function 'S<A>::~S(void)'
          <proj_path>\ConcreteS.h(8) : see reference to class template instantiation 'S<A>' being compiled
<proj_path>\S.h(10): error C2227: left of '->foo' must point to class/struct/union/generic type [<proj_path>\build_vs13\tmp.vcxproj]
  Generating Code...

Question: Who is right? Visual Studio or GCC/Clang?

For reference, I also post the declaration and definition of struct A and my CMakeLists.txt:

// A.h
#pragma once

struct A
{
    void foo() const;
};
// A.cpp
#include "A.h"
#include <cstdio>

void A::foo() const { std::puts("A::foo()"); }
# CMakeLists.txt
cmake_minimum_required(VERSION 3.6)
project(tmp)

add_executable(tmp A.cpp A.h ConcreteS.cpp ConcreteS.h S.h main.cpp)

if (MSVC)
    target_compile_options(tmp PRIVATE /W4 /WX /permissive-)
else()
    target_compile_options(tmp PRIVATE -Wall -Wextra -pedantic -Werror)
endif()

CodePudding user response:

Using S<A> as base class causes it to be implicitly instantiated.

Generally that wouldn't be a problem since your class doesn't require A to be complete when it is instantiated.

However, the definition of your destructor for S<A> requires A to be complete (because of the member access). That would normally also not be a problem, because generally definitions of member functions are not implicitly instantiated with implicit instantiation of the class template specialization, but only when they are used in a context requiring a definition to exist or in the case of the destructor when it is potentially invoked.

However, your destructor is virtual. For virtual member functions in particular it is unspecified whether they are instantiated with implicit instantiation of the containing class. ([temp.inst]/11)

So, the implementation may or may not choose to instantiate S<A>::~S<A> in the translation unit for main.cpp. If it does, the program will fail to compile since the member access in the definition is ill-formed for an incomplete type.

In other words, it is unspecified whether or not the program is valid and all mentioned compilers behave conforming to the standard.

If you remove virtual (and override) on the destructor(s) the only instantiation of the S<A> destructor allowed will be in the ConcreteS.cpp translation unit where A is complete and instantiation valid. The program is then valid and it should compile under MSVC as well.

  • Related