Prevent instantiation of template class with an incomplete type

强颜欢笑 提交于 2019-12-23 19:34:14

问题


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?


回答1:


Don't try and prevent instantiations with undefined types explicitly, let the compiler do its job. If you try and prevent instantiations with undefined types manually you might risk violating ODR, for more look here if-else depends on whether T is a complete type

You can dump the values in the vector in unique_ptrs to add a layer of indirection. For more information about how unique_ptr works with incomplete types look here std::unique_ptr with an incomplete type won't compile For example

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() {}

Also consider reading this blog post http://www.gotw.ca/gotw/028.htm, it outlines an alternative to dynamic allocation if you are against that




回答2:


ATTENTION, POSSIBLE DARK MAGIC: I am not sure whether (or since when) one is allowed - by the standard - to define a member std::vector<T> with an incomplete type T if none of the vectors member functions (including constructors and destructor) is referenced from the current translation unit. I think C++17 allows this, though.

One possibility that compiles since C++11 (though it could be illegal, see above) is the use of an union member to avoid calls to both the constructor(s) as well as to the destructor of the std::vector member:

struct Hidden;

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

Now, in your implementation file, you can (and must) actually call the constructor(s) and destructor of the vector:

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

Of course, using the member d.v in any way will need the definition of Hidden. Thus you should limit such uses to the (non inline) member functions of Public, which you implement in file(s) that have access to the complete definition of Hidden.




回答3:


What you need is enough space for the vector, and all vectors in every implementation I know of take the same space regardless of what they store.

We can exploit this, while asserting we are right.

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

in 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>();
}

Also manually write copy/move ctor/assign.

We might be able to automate this, but it is tricky; we want the fact we are really a vector of B to only cause code to be created in A.cpp, nowhere else.



来源:https://stackoverflow.com/questions/44632652/prevent-instantiation-of-template-class-with-an-incomplete-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!