防止C ++(或C ++ 0x)中的头爆炸

时间:2011-03-28 14:08:19

标签: c++ pointers c++11 smart-pointers raii

让我们说具有如下通用代码:

y.hpp:

#ifndef Y_HPP
#define Y_HPP

// LOTS OF FILES INCLUDED

template <class T>
class Y 
{
public:
  T z;
  // LOTS OF STUFF HERE
};

#endif

现在,我们希望能够在我们创建的类(比如X)中使用Y.但是,我们不希望X的用户必须包含Y标头。

所以我们定义了一个类X,类似这样:

x.hpp:

#ifndef X_HPP
#define X_HPP

template <class T>
class Y;

class X
{
public:
  ~X();
  void some_method(int blah);
private:
  Y<int>* y_;
};

#endif

请注意,因为y_是指针,所以我们不需要包含它的实现。

实现在x.cpp中,它是单独编译的:

x.cpp:

#include "x.hpp"
#include "y.hpp"

X::~X() { delete y_; }
void X::someMethod(int blah) { y_->z = blah; }

所以现在我们的客户可以只包含“x.hpp”来使用X,而不包括并且必须处理所有“y.hpp”标题:

main.cpp:

#include "x.hpp"

int main() 
{
  X x;
  x.blah(42);
  return 0; 
}

现在我们可以单独编译main.cppx.cpp,在编译main.cpp时,我不需要包含y.hpp

但是使用这段代码我必须使用原始指针,而且我必须使用删除。

所以这是我的问题:

(1)有没有办法让Y成为X的直接成员(不是Y的指针),而不需要包含Y标题? (我强烈怀疑这个问题的答案是否定的)

(2)有没有办法可以使用智能指针类来处理堆分配的Y? unique_ptr似乎是明显的选择,但是当我更改x.hpp

中的行时

从:

Y<int>* y_; 

为:

std::unique_ptr< Y<int> > y_;

并包含,并使用c ++ 0x模式编译,我收到错误:

/usr/include/c++/4.4/bits/unique_ptr.h:64: error: invalid application of ‘sizeof’ to incomplete type ‘Y<int>’ 
/usr/include/c++/4.4/bits/unique_ptr.h:62: error: static assertion failed: "can't delete pointer to incomplete type"

那么无论如何通过在自定义析构函数中使用标准智能指针而不是原始指针以及原始删除来实现此目的吗?

解决方案:

Howard Hinnant说得对,我们需要做的就是以下列方式改变x.hppx.cpp

x.hpp:

#ifndef X_HPP
#define X_HPP

#include <memory>

template <class T>
class Y;

class X
{
public:
  X(); // ADD CONSTRUCTOR FOR X();
  ~X();
  void some_method(int blah);
private:
  std::unique_ptr< Y<int> > y_;
};

#endif

x.cpp:

#include "x.hpp"
#include "y.hpp"

X::X() : y_(new Y<int>()) {} // ADD CONSTRUCTOR FOR X();
X::~X() {}
void X::someMethod(int blah) { y_->z = blah; }

我们很高兴使用unique_ptr。谢谢霍华德!

解决方案背后的理由:

如果我错了,人们可以纠正我,但是这个代码的问题是隐式默认构造函数试图默认初始化Y,并且因为它对Y没有任何了解,所以它不能这样做。通过明确地说我们将在其他地方定义构造函数,编译器认为“好吧,我不必担心构造Y,因为它是在其他地方编译的”。

真的,我应该首先添加一个构造函数,如果没有它,我的程序就会出错。

3 个答案:

答案 0 :(得分:12)

您可以使用unique_ptrshared_ptr来处理不完整的类型。如果您使用shared_ptr,则必须按照惯例勾勒~X()。如果您使用unique_ptr,则必须概述~X()X()(或者您用于构建X的任何构造函数)。隐式生成的X默认ctor要求完整类型Y<int>

shared_ptrunique_ptr都可以防止您在不完整类型上意外调用delete。这使它们优于原始指针,它不提供这种保护。 unique_ptr需要概述X()的原因归结为它具有静态删除而不是动态删除。

编辑:更深入的澄清

由于unique_ptrshared_ptr的静态删除与动态删除差异,两个智能指针要求element_type在不同的地方完成。

unique_ptr<A>要求A完成:

  • ~unique_ptr<A>();

但不是为了:

  • unique_ptr<A>();
  • unique_ptr<A>(A*);

shared_ptr<A>要求A完成:

  • shared_ptr<A>(A*);

但不是为了:

  • shared_ptr<A>();
  • ~shared_ptr<A>();

最后,隐式生成的X() ctor将调用智能指针默认ctor 智能指针dtor(如果X()抛出异常 - 即使我们知道它不会。)

结论:X调用智能指针成员且element_type需要完成的任何成员必须概述到element_type完成的来源。

关于unique_ptrshared_ptr的一个很酷的事情是,如果你猜错了需要概述的内容,或者你没有意识到某个特殊成员被隐式生成需要一个完整的element_type,这些智能指针会告诉你一个(有时措辞不好)编译时错误。

答案 1 :(得分:7)

1)你是对的,答案是“不”:编译器应该知道成员对象的大小,如果没有Y类型的定义就不能知道它。

2)boost::shared_ptr(或tr1::shared_ptr)不需要完整类型的对象。因此,如果你能承受它所暗示的开销,那将有所帮助:

  

类模板在T上进行参数化,T是指向的对象的类型。 shared_ptr及其大多数成员函数对T没有要求;它被允许是一个不完整的类型,或无效。

修改:检查了unique_ptr个文档。似乎您可以使用它:只需确保~X()已定义unique_ptr<>的构建位置。

答案 2 :(得分:0)

如果您不喜欢使用pimpl习语所需的额外指针,请尝试此变体。首先,将X定义为抽象基类:

// x.hpp, guard #defines elided

class X
{
protected:
    X();

public:
    virtual ~X();

public:
    static X * create();
    virtual void some_method( int blah ) = 0;
};

请注意,此处不显示Y.然后,创建一个派生自X:

的impl类
 #include "Y.hpp"
    #include "X.hpp"

class XImpl 
: public X
{
    friend class X;

private:
    XImpl();

public:
    virtual ~XImpl();

public:
    virtual void some_method( int blah ) = 0;

private:
    boost::scoped_ptr< Y< int > > m_y;
};

X声明了一个工厂函数create()。实现它以返回XImpl:

// X.cpp

#include "XImpl.h"

X * X::create()
{
    return new XImpl();
}

X的用户可以包含X.hpp,它没有包含y.hpp。你得到的东西看起来有点像pimpl,但它没有明确的指向impl对象的额外指针。