4

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

请注意,我打算使用我的实际类作为基类,因此是虚拟析构函数;我怀疑这很重要,但我把它留在柜台以防万一。上面的结果输出是

counted: 5
counted: 3.2
counted: (2,4)
total count=3

NowCounter的类声明都可以放在头文件中(例如,counter.h)。我可以把需要iostream的dtor的实现放到counter.cpp中。但是对于countAndPrint()同样使用 iostream 的成员函数 template 怎么办?它在 counter.cpp 中没有用,因为它需要在已编译的 counter.o 之外进行实例化。但是将它放在 counter.h 中意味着包括 counter.h 在内的任何内容也反过来包含 iostream,这似乎是错误的(我接受我可能只需要克服这种厌恶)。我也可以将模板代码放到一个单独的文件中(counter.t?),但这对于代码的其他用户来说有点令人惊讶。Lakos并没有像我想要的那样深入探讨这个问题, C++ 常见问题解答没有进入最佳实践。所以我追求的是:

  1. 是否有任何替代方法可以将代码划分为我建议的代码?
  2. 在实践中,什么最有效?
4

3 回答 3

5

经验法则(其原因应该很清楚)。

  • 私有成员模板应该在 .cpp 文件中定义(除非它们需要被你的类模板的朋友调用)。
  • 非私有成员模板应在标头中定义,除非它们被显式实例化。

您通常可以通过使名称相互依赖来避免包含大量标题,从而延迟查找和/或确定它们的含义。这样,您仅在实例化时才需要完整的标头集。举个例子

#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;
      }
      // ...
    }
  }
};
于 2010-11-06T05:12:14.820 回答
1

Google Style Guide建议将模板代码放在“counter-inl.h”文件中。如果你想非常小心你的包含,那可能是最好的方法。

但是,客户端通过“意外”获得包含的iostream标头可能是一个很小的代价,因为您将所有类的代码放在一个逻辑位置 - 至少如果您只有一个成员函数模板。

于 2010-11-06T04:50:15.590 回答
1

实际上,您唯一的选择是将所有模板代码放在标头中,或者将模板代码放在.tcc文件中并将该文件包含在标头的末尾

此外,如果可能的话,您应该尽量避免在头文件中使用#includeing <iostream>,因为这会对编译时间造成重大影响。#include毕竟,头文件通常由多个实现文件组成。您在标题中需要的唯一代码是模板和内联代码。析构函数不需要在标题中。

于 2010-11-06T04:53:48.790 回答