Prevent instantiation of template class with an incomplete type

时间:2017-06-19 14:00:15

标签: c++ templates instantiation incomplete-type

I'm writing a library. Its layout looks something akin to this:

/////////
// A.h //
/////////

#include <vector>

class B;

class A
{
    std::vector<B> Bs;

public:
    ...
};

/////////
// B.h //
/////////

class B
{
    ...
}

///////////
// A.cpp //
///////////

#include "A.h"
#include "B.h"

// Implementation of A follows
...

///////////
// B.cpp //
///////////

#include "B.h"

// Implementation of B follows
...

/////////////
// MyLib.h //
/////////////

#include "A.h"

As you can see, the only type accessible from the outside is supposed to be A, that's why B is declared as an incomplete type in A.h. The library itself compiles fine, but when it comes to using it in a program, the compiler issues errors like: invalid use of incomplete type B when I try to create an object of type A. These errors point to the vector header, specifically, to the std::vector's destructor, which apparently needs to know the size of the type it holds to deallocate the internal storage properly. What I think is happening is that the compiler is trying to instantiate std::vector<B>::~vector in my program, which can't succeed for aforementioned reasons. However, there are symbols for std::vector<B>::~vector in the library's binary, so I could do without instantiating it in the program. I'm looking for a way to tell that to the compiler. I've already tried changing MyLib.h to something like this:

/////////////
// MyLib.h //
/////////////

#include "A.h"
extern template class std::vector<B>;

This unfortunately doesn't work, because extern applies only to the code generation stage of the compilation, and the compiler still reports errors while parsing. I thought that maybe the compiler is trying to instantiate std::vector<B>::~vector because A has an implicit destructor, so I tried implementing a destructor manually like this:

/////////
// A.h //
/////////

#include <vector>

class B;

class A
{
    std::vector<B> Bs;
    ~A();

public:
    ...
};

///////////
// A.cpp //
///////////

#include "A.h"
#include "B.h"

A::~A() {}

// Further implementation of A follows
...

This doesn't help either, the compiler is still trying to instantiate std::vector<B>::~vector, even though it shouldn't ever be invoked outside the library code now. Is there a different way to achieve what I want or would it be best to choose a different method of information hiding for B?

3 个答案:

答案 0 :(得分:2)

不要试图明确地防止使用未定义类型的实例化,让编译器完成它的工作。如果您尝试手动阻止使用未定义类型的实例化,则可能存在违反ODR的风险,请在此处查看if-else depends on whether T is a complete type

您可以将值转储到unique_ptr中的向量中以添加一个间接层。有关unique_ptr如何使用不完整类型的详细信息,请在此处查看std::unique_ptr with an incomplete type won't compile例如

的main.cpp

#include <iostream>
#include <vector>
#include <memory>

#include "something.hpp"

using std::cout;
using std::endl;

int main() {

    Something something;

    return 0;
}

something.hpp

#pragma once

#include <vector>
#include <memory>

class Incomplete;
class Something {
public:
    Something();
    ~Something();
    std::vector<std::unique_ptr<Incomplete>> incompletes;
};

something.cpp

#include "something.hpp"

class Incomplete {};

Something::Something() {}
Something::~Something() {}

另请考虑阅读此博文http://www.gotw.ca/gotw/028.htm,如果您反对,则概述动态分配的替代方案

答案 1 :(得分:2)

注意,可能的黑暗魔法: I am not sure whether(或从何时开始)允许 - 按标准 - 定义具有不完整类型的成员std::vector<T> T如果从当前翻译单元引用了向量成员函数 none 包括构造函数和析构函数)。我认为C ++ 17允许这样做。

从C ++ 11开始编译的一种可能性(虽然它可能是非法的,见上文)是使用union成员来避免调用构造函数和析构函数std::vector成员:

struct Hidden;

struct Public {
    union Defer {
        std::vector<Hidden> v;
        Defer();
        // add copy/move constructor if needed
        ~Defer();
    } d;
};

现在,在您的实现文件中,您可以(并且必须)实际调用向量的构造函数和析构函数:

struct Hidden { /* whatever */ };
Public::Defer::Defer() { new (&v) std::vector<Hidden>(); }
Public::Defer::~Defer() { v.~vector<Hidden>(); }

当然,以任何方式使用成员d.v都需要Hidden 的定义。因此,您应该将此类用途限制为inline的{​​non Public)成员函数,您可以在可以访问Hidden的完整定义的文件中实现这些函数。

答案 2 :(得分:1)

你需要的是足够的向量空间,我所知道的每个实现中的所有向量都占用相同的空间,无论它们存储什么。

我们可以利用这一点,同时断言我们是对的。

struct A {
  std::aligned_storage_t<sizeof(std::vector<int>), alignof(std::vector<int>)> v;
  A();
  ~A();
};

在A.cpp中

#include<b.h>
static_assert(sizeof(std::vector<B>)==sizeof(std::vector<int>), "size mismatch");
static_assert(alignof(std::vector<B>)==alignof(std::vector<int>), "align mismatch");

std::vector<B>& get_v(A& a){ return *(std::vector<B>*)&a.v; }
std::vector<B> const& get_v(A const& a){ return *(std::vector<B> const*)&a.v; }

A::A(){
  ::new ((void*)&v) std::vector<B>();
  try{
    // rest of ctor
  }catch(...){
    get_v(*this).~std::vector<B>();
    throw;
  }
}
A::~A(){
  get_v(*this).~std::vector<B>();
}

还手动写入复制/移动ctor / assign。

我们或许能够实现自动化,但这很棘手;我们希望事实上我们真的是B的向量,只能导致在A.cpp中创建代码,而不是其他地方。