C ++的一个方面经常让我感到沮丧的是决定模板在头文件(传统上描述接口)和实现(.cpp)文件之间的位置。模板通常需要进入标题,暴露实现,有时会引入额外的标头,以前只需要包含在.cpp文件中。我最近又遇到了这个问题,下面给出了一个简单的例子。
#include <iostream> // for ~Counter() and countAndPrint()
class Counter
{
unsigned int count_;
public:
Counter() : count_(0) {}
virtual ~Counter();
template<class T>
void
countAndPrint(const T&a);
};
Counter::~Counter() {
std::cout << "total count=" << count_ << "\n";
}
template<class T>
void
Counter::countAndPrint(const T&a) {
++count_;
std::cout << "counted: "<< a << "\n";
}
// Simple example class to use with Counter::countAndPrint
class IntPair {
int a_;
int b_;
public:
IntPair(int a, int b) : a_(a), b_(b) {}
friend std::ostream &
operator<<(std::ostream &o, const IntPair &ip) {
return o << "(" << ip.a_ << "," << ip.b_ << ")";
}
};
int main() {
Counter ex;
int i = 5;
ex.countAndPrint(i);
double d=3.2;
ex.countAndPrint(d);
IntPair ip(2,4);
ex.countAndPrint(ip);
}
请注意,我打算使用我的实际类作为基类,因此使用虚拟析构函数;我怀疑这很重要,但为了以防万一,我把它留在了Counter中。上面得到的结果是
counted: 5
counted: 3.2
counted: (2,4)
total count=3
现在Counter
的类声明都可以放在头文件中(例如,counter.h)。我可以把需要iostream的dtor的实现放到counter.cpp中。但是对于也使用iostream的成员函数模板countAndPrint()
该怎么办?它在counter.cpp中没用,因为它需要在编译的counter.o之外实例化。但是将它放在counter.h中意味着包括counter.h在内的任何东西也包括iostream,这似乎是错误的(我接受我可能只需要克服这种厌恶)。我也可以将模板代码放入一个单独的文件(counter.t?),但这对代码的其他用户来说有点令人惊讶。 Lakos并没有像我想的那样真正地进入这个问题,C++ FAQ没有进入最佳实践。所以我要追求的是:
答案 0 :(得分:5)
经验法则(其原因应该清楚)。
通常可以通过使名称依赖来包含大量标题,从而延迟查找和/或确定其含义。这样,只需在实例化时就需要完整的标头集。作为一个例子
#include <iosfwd> // suffices
class Counter
{
unsigned int count_;
public:
Counter() : count_(0) {}
virtual ~Counter();
// in the .cpp file, this returns std::cout
std::ostream &getcout();
// makes a type artificially dependent
template<typename T, typename> struct ignore { typedef T type; };
template<class T>
void countAndPrint(const T&a) {
typename ignore<std::ostream, T>::type &cout = getcout();
cout << count_;
}
};
这是我用于实现使用CRTP的访问者模式的原因。它最初看起来像
template<typename Derived>
struct Visitor {
Derived *getd() { return static_cast<Derived*>(this); }
void visit(Stmt *s) {
switch(s->getKind()) {
case IfStmtKind: {
getd()->visitStmt(static_cast<IfStmt*>(s));
break;
}
case WhileStmtKind: {
getd()->visitStmt(static_cast<WhileStmt*>(s));
break;
}
// ...
}
}
};
由于这些静态强制转换,这将需要所有语句类的标头。所以我已经使类型依赖,然后我只需要前向声明
template<typename T, typename> struct ignore { typedef T type; };
template<typename Derived>
struct Visitor {
Derived *getd() { return static_cast<Derived*>(this); }
void visit(Stmt *s) {
typename ignore<Stmt, Derived>::type *sd = s;
switch(s->getKind()) {
case IfStmtKind: {
getd()->visitStmt(static_cast<IfStmt*>(sd));
break;
}
case WhileStmtKind: {
getd()->visitStmt(static_cast<WhileStmt*>(sd));
break;
}
// ...
}
}
};
答案 1 :(得分:1)
Google Style Guide建议将模板代码放在“counter-inl.h”文件中。如果您想对包含内容非常小心,那可能是最好的方式。
但是,客户通过“意外”获得包含的iostream
标题可能是将所有类的代码放在一个逻辑位置上的一小部分代价 - 至少如果您只有一个成员函数模板。
答案 2 :(得分:1)
实际上,您唯一的选择是将所有模板代码放在标题中,或将模板代码放在.tcc
文件中,并在标题末尾包含该文件 。
此外,如果可能,您应该尽量避免在标头中#include
<iostream>
,因为这会对编译时造成重大影响。毕竟,标题通常由多个实现文件#include
组成。标题中唯一需要的代码是模板和内联代码。析构函数不需要在标题中。