起初我对这个问题很感兴趣,因为它看起来非常棘手,而且所有关于模板、依赖项和包含的评论都是有意义的。但后来我尝试实际实现这一点,发现它非常容易。所以要么我误解了这个问题,要么这个问题有一些特殊的属性,看起来比它实际上更难。无论如何,这是我的代码。
这是美化的 autoptr.h:
#ifndef TESTPQ_AUTOPTR_H
#define TESTPQ_AUTOPTR_H
template<class T> class AutoPtr {
private:
T *p;
public:
AutoPtr() {p = new T();}
~AutoPtr() {delete p;}
T *operator->() {return p;}
};
#endif // TESTPQ_AUTOPTR_H
看起来很简单,我想知道它是否真的有效,所以我为它做了一个测试用例。这是我的 bh:
#ifndef TESTPQ_B_H
#define TESTPQ_B_H
class B {
public:
B();
~B();
void doSomething();
};
#endif // TESTPQ_B_H
和 b.cpp:
#include <stdio.h>
#include "b.h"
B::B()
{
printf("B::B()\n");
}
B::~B()
{
printf("B::~B()\n");
}
void B::doSomething()
{
printf("B does something!\n");
}
现在对于实际使用它的 A 类。这里啊:
#ifndef TESTPQ_A_H
#define TESTPQ_A_H
#include "autoptr.h"
class B;
class A {
private:
AutoPtr<B> b;
public:
A();
~A();
void doB();
};
#endif // TESTPQ_A_H
和 a.cpp:
#include <stdio.h>
#include "a.h"
#include "b.h"
A::A()
{
printf("A::A()\n");
}
A::~A()
{
printf("A::~A()\n");
}
void A::doB()
{
b->doSomething();
}
好的,最后是使用 A 但不包含“bh”的 main.cpp:
#include "a.h"
int main()
{
A a;
a.doB();
}
现在它实际上编译时没有任何错误或警告并且可以正常工作:
d:\alqualos\pr\testpq>g++ -c -W -Wall b.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
d:\alqualos\pr\testpq>g++ -o a a.o b.o main.o
d:\alqualos\pr\testpq>a
B::B()
A::A()
B does something!
A::~A()
B::~B()
这能解决你的问题还是我在做一些完全不同的事情?
编辑1:它是标准的还是不标准的?
好吧,这似乎是正确的,但现在它引导我们提出其他有趣的问题。这是我们在下面的评论中讨论的结果。
在上面的例子中会发生什么?ah 文件不需要 bh 文件,因为它实际上并没有对 做任何事情b
,它只是声明它,并且它知道它的大小,因为 AutoPtr 类中的指针总是相同的大小。autoptr.h 中唯一需要定义 B 的部分是构造函数和析构函数,但它们没有在 ah 中使用,因此 ah 不需要包含 bh
但是为什么 ah 不使用 B 的构造函数呢?每当我们创建 A 的实例时,B 的字段不是都初始化了吗?如果是这样,编译器可能会尝试在 A 的每个实例化时内联此代码,但随后它将失败。在上面的示例中,看起来B::B()
调用是放在A::A()
a.cpp 单元中已编译构造函数的开头,但标准是否需要它?
起初,似乎没有什么能阻止编译器在创建瞬间时内联字段初始化代码,所以A a;
变成了这个伪代码(当然不是真正的 C++):
A a;
a.b->B();
a.A();
这样的编译器可以按照标准存在吗?答案是否定的,他们不能,标准与此无关。当编译器编译“main.cpp”单元时,它不知道 A::A() 构造函数做了什么。它可能会为 调用一些特殊的构造函数,因此在使用不同的构造函数进行两次初始化b
之前内联默认构造函数!b
并且编译器无法检查它,因为A::A()
定义的“a.cpp”单元是单独编译的。
好的,现在你可能会想,如果一个智能编译器想要查看 B 的定义,并且除了默认的构造函数之外没有其他构造函数,那么它不会B::B()
在构造函数中放置任何调用,A::A()
而是在调用时内联它A::A()
。好吧,这也不会发生,因为编译器无法保证即使 B 现在没有任何其他构造函数,将来也不会有任何其他构造函数。假设我们将这个添加到 B 类定义中的 bh 中:
B(int b);
然后我们把它的定义放到b.cpp中,并相应地修改a.cpp:
A::A():
b(17) // magic number
{
printf("A::A()\n");
}
现在当我们重新编译 a.cpp 和 b.cpp 时,即使我们不重新编译 main.cpp,它也会按预期工作。这就是所谓的二进制兼容性,编译器不应该破坏它。但如果它内联B::B()
调用,我们最终会得到调用两个B
构造函数的 main.cpp。但是由于添加构造函数和非虚拟方法不应该破坏二进制兼容性,任何合理的编译器都不应该被允许这样做。
此类编译器不存在的最后一个原因是它实际上没有任何意义。即使成员初始化是内联的,它只会增加代码大小并且绝对不会提高性能,因为仍然需要一个方法调用,A::A()
那么为什么不让这个方法在一个地方完成所有工作呢?
编辑 2:好的,A 的内联和自动生成的构造函数呢?
A:A()
出现的另一个问题是,如果我们同时从 ah 和 a.cpp中删除会发生什么?这是发生的事情:
d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
In file included from a.h:4:0,
from main.cpp:1:
autoptr.h: In constructor 'AutoPtr<T>::AutoPtr() [with T = B]':
a.h:8:9: instantiated from here
autoptr.h:8:16: error: invalid use of incomplete type 'struct B'
a.h:6:7: error: forward declaration of 'struct B'
autoptr.h: In destructor 'AutoPtr<T>::~AutoPtr() [with T = B]':
a.h:8:9: instantiated from here
autoptr.h:9:17: warning: possible problem detected in invocation of delete
operator:
autoptr.h:9:17: warning: invalid use of incomplete type 'struct B'
a.h:6:7: warning: forward declaration of 'struct B'
autoptr.h:9:17: note: neither the destructor nor the class-specific operator
delete will be called, even if they are declared when the class is defined.
唯一相关的错误消息是“无效使用不完整类型'struct B'”。基本上这意味着 main.cpp 现在需要包含 bh,但为什么呢?因为在我们实例化的时候自动生成的构造函数是内联的a
在 main.cpp 中。好的,但这总是必须发生还是取决于编译器?答案是它不能依赖于编译器。没有编译器可以使自动生成的构造函数非内联。原因是它不知道将代码放在哪里。从程序员的角度来看,答案很明显:构造函数应该放在定义类的所有其他方法的单元中,但编译器不知道是哪个单元。此外,类方法可以分布在多个单元中,有时甚至是有意义的(例如,如果类的一部分是由某些工具自动生成的)。
当然,如果我们A::A()
通过使用 inline 关键字或将其定义放在 A 类声明中来显式地内联,则会发生相同的编译错误,可能会不那么神秘。
结论
将上述技术用于自动实例化指针似乎完全没问题。我唯一不确定的是AutoPtr<B> b;
ah 里面的东西可以与任何编译器一起使用。我的意思是,我们可以在声明指针和引用时使用前向删除类,但是将它用作模板实例化参数总是正确的吗?我认为这没有错,但编译器可能会不这么认为。谷歌搜索也没有产生任何有用的结果。