【问题标题】:Where to put a member function template成员函数模板的放置位置
【发布时间】:2010-11-06 04:42:49
【问题描述】:

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

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

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

【问题讨论】:

    标签: c++ member-functions function-templates


    【解决方案1】:

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

    • 应该在 .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;
          }
          // ...
        }
      }
    };
    

    【讨论】:

    • 那么这是否意味着您仍然需要标头才能实例化模板?我认为将标题分成两部分(实际需要额外包含的部分和不需要的部分)对于客户来说比说“你可以包含我的标题,但为了要使用它的某些部分,您还必须包含 XYZ"。还是说他们的 .h 文件不需要额外的东西,而他们的 .cpp 文件需要额外的东西是自然而然的?
    • @Steve 例如,我的访问者的客户派生自 Visitor - 只是派生不需要任何额外的标头。调用visit 时需要它们。但是调用visit 可以由客户在他们的.cpp 文件中完成,这是他们将包含所有标题的地方。所以在任何情况下,头文件中都不会包含额外的头文件。同样,在Counter 示例中,仅在调用countAndPrint 时才需要它们。我认为这肯定有助于减少依赖和编译时间。
    • 很公平 - 所以问题是这样做是否更方便(如果客户想打电话给&lt;iostream&gt;,他们需要知道包括&lt;iostream&gt;),或者只是把定义countAndPrint 在不同的头文件中,其中包括 &lt;iostream&gt;。当然,或者只是将带有附加模板参数的std::cout 传递给countAndPrint,并省去任何依赖关系的麻烦;-)
    • 非常感谢您为我提供了超出我能想到的更多选择。我认为在清晰度方面这里有一个权衡 - getcoutignore 肯定会帮助我,但会给其他阅读代码的人带来负担。但我认为这对你来说是 C++。
    【解决方案2】:

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

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

    【讨论】:

    • 良好的链接 (+1)。我有时会反对他们的一些风格指南,但这很有用。
    • Google 似乎更改了样式指南。现在是headers should be self contained
    • 为了便于阅读,我将引用 manlio 链接到的文本(对我有帮助的部分):If a template or inline function is declared in a .h file, define it in that same file. The definitions of these constructs must be included into every .cc file that uses them, or the program may fail to link in some build configurations. Do not move these definitions to separate -inl.h files.
    • Google C++ Style Guide 已更新:“不要将这些定义移动到单独包含的头文件 (-inl.h);这种做法在过去很常见,但已不再被允许。”
    【解决方案3】:

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

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

    【讨论】:

    • 好吧,因此我不喜欢将定义放入标题中。
    • 顺便说一句,这一切都让我思考,如果 C++ 允许从需要库的函数中#include,而不是将它们全部放在文件。这将更符合从函数范围而不是文件范围的命名空间导入的偏好。听起来可能很邪恶,但如果我知道 &lt;iostream&gt; 只会在模板方法被实例化时包含,我不会介意将模板放入标题中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-02
    • 2017-10-31
    • 1970-01-01
    • 1970-01-01
    • 2014-02-03
    相关资源
    最近更新 更多