问题: 在使用boost::scoped_ptr时模板参数使用前置声明类型,编译器报错。
解决方式: 需要显式声明析构函数,并在实现文件中定义析构函数,以避免编译器自动为我们生成析构函数.
详情:
前置声明是个好习惯,既可以减少编译依赖,又可以避免循环包含带来的问题,其本身也是最小化原则的体现。
如果你已经把前置声明当作一种习惯而且经常使用boost的智能指针或者曾遇到过类似checked_delete之类的东西,你应该遇到过这个问题,你的解决方案有可能是直接包含头文件(我以前也这么干,不过这次是解决别人丢过来的问题,就不能这么草率了),但是你是否深知问题出现的根本原因呢?
关键字: incomplete type, delete, forward declaration, boost::scoped_ptr, boost::checked_delete
C++, non-trivial destructor.
(不完全类型, 自动生成析构函数)。
示例代码(第三方库代码只保留核心部分,其他略去,已通过测试):
//scoped_ptr.h
#ifndef _SCOPED_PTR_H_
#define _SCOPED_PTR_H_
template<class T>
inline void checked_delete(T * x)
{
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete x;
}
template<class T>
class scoped_ptr // noncopyable
{
private:
T * px;
public:
explicit scoped_ptr( T * p = 0 ): px( p )
{
}
~scoped_ptr()
{
checked_delete( px );
}
};
#endif // _SCOPED_PTR_H_
/A.h
#ifndef _A_H_
#define _A_H_
class A
{
public:
~A();
};
#endif //_A_H_
/A.cpp
#include "A.h"
#include <iostream>
A::~A()
{
std::cout << "A::~A" << std::endl;
}
/B.h
#ifndef _B_H_
#define _B_H_
#include "scoped_ptr.h"
class A;
class B
{
public:
B();
private:
scoped_ptr<A> m_pA;
};
#endif //_B_H_
/B.cpp
#include "B.h"
#include <iostream>
#include "A.h"
B::B() : m_pA(new A)
{}
///main.cpp/
#include <iostream>
#include "B.h"
int main()
{
B b;
}
编译main.cpp,编译器报错
In file included from B.h:4:0,
from main.cpp:3:
scoped_ptr.h: In function 'void checked_delete(T*) [with T = A]':
scoped_ptr.h:25:8: instantiated from 'scoped_ptr<T>::~scoped_ptr() [with T = A]'
B.h:8:7: instantiated from here
scoped_ptr.h:8:18: error: invalid application of 'sizeof' to incomplete type 'A'
scoped_ptr.h:10:5: warning: possible problem detected in invocation of delete operator: [enabled by default]
scoped_ptr.h:5:13: warning: 'x' has incomplete type [enabled by default]
B.h:6:7: warning: forward declaration of 'struct A' [enabled by default]
scoped_ptr.h:10:5: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
根据错误提示,显然编译器发现A是incomplete type, sizeof操作符无法计算A对象的大小。
需要关注的是当前报错的编译单元是main.cpp, 报错的是scoped_ptr<T>::~scoped_ptr() [with T = A], 这就是说在main.cpp这个编译单元中使用到了scoped_ptr<A>的析构函数,而scoped_ptr<A>作为B的成员,其析构函数只能由B的析构函数触发(c++的构造是从部分到整体,析构则是从整体到部分),也就是说main.cpp这个编译单元出现了B的析构函数。
显然我们的代码中并没有B的析构函数,那只有一种可能,就是编译器帮助我们插入了析构函数,且是以inline public (见C++标准12.4.4)方式插入(可以理解为插入在B的头文件中)。【如果你要问编译器为什么不将析构函数实现插入到B.cpp中,那就请站在编译器的角度思考下,因为对于编译器而言,B.h和B.cpp并没有什么必然的关系, 只是程序员为了便于管理工程而命的文件名】。 这也合情合理的,由于B的成员scoped_ptr有自定义的析构函数,编译器就有责任为B插入析构函数,并在其中插入对scoped_ptr析构函数的调用。
现在知道了问题的原因,自然就有了解决方案,将B的析构函数转移到B.h之外的任何编译单元(一般正常的程序员会选择B.cpp),这样编译器就不会在B.h插入类B的析构函数了(因为已经有了嘛)。而scoped_ptr<A>的析构函数调用需求也被转移到了B.cpp这个编译单元中,而在这里有对A.h的包含,编译器看得到A的完整定义,所以也就不会报错了。
即最终解决方案如下:
1. 在B.h声明析构函数。(即使什么也不做,也不能放在头文件中,否则跟编译器自动插入的析构函数无异,都会在包含B.h的编译单元中报错)。
2. 在B.cpp中定义析构函数B::~B()的实现。
除了解决以上问题,还有几点需要明确:
1. 编译器自动生成析构函数(或构造函数)的原则:
C++早期标准定义并不是程序员不定义编译器就会自动生成,而是在有需要的情况下(编译器有需要而不是程序员有需要)。最新的C11与此稍有差异,由于有了最新的default和delete语义,所以只要用户不定义,编译器就会为其自动生成,但若是自动生成的具有delete语义的构造函数或析构函数被调用到了,编译器会报错。
//x.cpp
struct A
{
~A() {};
};
union B
{
private:
A a;
};
int main()
{
B b;
}
编译器给出以下错误提示。
x.cpp:9:11: error: member 'A B::a' with destructor not allowed in union
x.cpp:9:11: note: unrestricted unions only available with -std=c++0x or -std=gnu++0x
报错内容没有涉及B的析构函数,只是在语法检测级别上定位错误,联合类型内部成员不能具有non-trival 析构函数。
而如果添加 -std=c++0x选项,报错如下
x.cpp: In function 'int main()':
x.cpp:13:7: error: use of deleted function 'B::~B()'
x.cpp:5:7: error: 'B::~B()' is implicitly deleted because the default definition would be ill-formed:
x.cpp:8:11: error: union member 'B::a' with non-trivial 'A::~A()'
编译器则告知自动生成的B的析构函数是implicitly deleted错误(其根本原因当然还是union的成员中不能有non-trival 析构函数。
可参考
[1] 《inside c++ object model 》 第2.1和5.5节
[2] C++ std 标准文档第12章。
2. 为什么在delete 指针或者指针数组时需要检查是否是incomplete type
C++标准规定(12.3.5/5)
If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined。
也就是说允许delete不完整类型对象,但其行为是未定义的。
如下代码按照C++标准是没有错误的,但是不同的编译器在检测到对不完全类型对象的删除时可能会给出不同的warning 信息:
//x.cpp
class A;
int main()
{
delete (A *)0x12121212;
}
Clang:
warning: deleting pointer to incomplete type 'A' may cause undefined
behaviour
delete (A *)0x12121212;
^ ~~~~~~~~~~~~~~~
x.cpp:1:7: note: forward declaration of 'A'
class A;
^
1 warning generated.
Gcc:
x.cpp: In function 'int main()':
x.cpp:5:18: warning: possible problem detected in invocation of delete operator: [enabled by default]
x.cpp:5:18: warning: invalid use of incomplete type 'struct A' [enabled by default]
x.cpp:1:7: warning: forward declaration of 'struct A' [enabled by default]
x.cpp:5:18: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
Gcc给出的warning信息看起来更详细些,当使用delete删除不完全类型对象的弊端在于不会调用该类的析构函数或者用户自定义的operator delete,为了避免这种情况,boost::scoped_ptr借用sizeof对不完全类型的静态检测实现了check_delete。
可参考:
[1] . C++标准12.3.5/5