【问题标题】:Is it ok to keep simple C++ method definitions inside of a header file?可以在头文件中保留简单的 C++ 方法定义吗?
【发布时间】:2020-09-02 12:02:41
【问题描述】:

我已将所有类放入单独的头文件中。所以我有一个名为snake 的文件用于Snake 类和一个fruit 文件用于Fruit 类等。我熟悉使用头文件声明类及其方法的概念,然后在具有相同名称的单独 .cpp 文件中定义方法。所有这些对我来说都很有意义,而且我过去也这样做过。

我的问题是:在没有单独的 .cpp 文件的情况下在头文件中实现最简单的方法是否被认为是一种不好的做法?或者也许我应该把整个班级放在.cpp 文件中?

我问这个的原因是我的一些类只包含 2 个或 3 个 setget 成员函数,我没有看到指向为少于 10 行代码创建一个单独的文件。事实上,我相信这会使我的代码变得不必要地复杂。我在网上看了,有人说可以,有人说不行。

【问题讨论】:

  • 模板方法不能在单独的cpp文件中定义,所以有些情况下在C++头中定义方法是好的。
  • 这本质上是风格问题。我个人认为简单的 getter/setter 在头文件中是可以的。但是,在您的情况下,您确定带有公共成员的简单结构不会更好吗?
  • 这是基于意见的。但是如果我关心性能,我倾向于在标题中定义函数,因为那时这些函数可能会被内联。
  • 如果你的类只有几个getter和setter,没有其他成员函数,你不妨去掉它们,使用公共成员变量。
  • “我的一些类只包含 2 个或 3 个 set 和 get 成员函数”——那么它们可能不是很好的类,可以从重构中受益。 OOP 的全部意义在于将 行为 与您的数据捆绑在一起。如果数据没有行为,它只是一袋值。这可能没问题,但是为什么还要麻烦 setter 和 getter 呢?

标签: c++ oop


【解决方案1】:
  1. 头文件和“源”文件之间的拆分仅适用于人类。就编译器而言,您需要一个源文件——一个翻译单元——比如foo.txt,并直接从中生成一个可执行文件。该文件可以具有任何名称 - 不需要以 .cpp.h 结尾 - 这也是为了让人类的生活更轻松。编译器一点也不在乎。

  2. 话虽如此,这完全取决于您以及您从事的项目的编码风格。您绝对肯定必须采取任何措施来提高代码的可读性、凝聚力和可理解性。编译器?那个最后来了。

【讨论】:

  • 嗯,头文件通常会编译多次,因此其中的任何函数定义都应该隐式或显式内联或静态以避免重复的符号定义。
  • 谢谢。那正是我所想。我想确保它在概念上没有错误。
  • @Marsellus 从概念上讲绝对不是。甚至在某些情况下,您必须将定义放在标题中(或根据此答案的“标题”;),例如用于函数模板或具有返回类型推导的函数。
  • @Peter-ReinstateMonica 当然,但这不是你可以不知道的事情。链接器会朝你尖叫。虽然,如果您对自己好,并从创建项目的那一刻起启用统一构建 - 通常应该加快编译速度 - 统一构建可能会隐藏该问题,直到构建系统决定更改源文件的分配到翻译单位。 Unity 构建可能是我遇到过的最多两面性的虫子。狡猾的小混蛋也一样。我仍然推荐他们——这比在现实生活中遇到像他们这样的人更好:)
【解决方案2】:

@ReinstateMonica 留下了一个非常好的、简洁的答案,我完全同意——但我想对此进行一些扩展。

在标题中定义内联函数本质上没有“好”或“坏”之分。在某些情况下,最好不要在标头中定义,因为它可能会导致代码膨胀;但这是在检查产品后确定的。

最重要的是您的代码易于阅读和理解,因为这提高了可维护性——无论是其他人担任维护者,还是一年后自己担任维护者,都无关紧要。

根据我在 C++ 大型项目中工作的经验,通常将头文件作为“动态文档”的形式来查看任何给定头文件中引入了哪些功能。让代码一目了然地易于消化,对于使用此 API 编写新代码的速度会有巨大的差异。

在类定义中内联编写的函数定义会打断自然流程,从而破坏可读性。例如,考虑以下两个等效的代码 sn-ps:

class Snake
{
public:

   auto get_name() -> std::string
   {
      return m_name;
   }

   auto eat(const Fruit& fruit) -> void
   {
        ..
        // some really long definition
        ..
   }

   auto set_name(std::string name)
   {
      m_name = std::move(name);
   }
};

将此与更简洁但仍是内联的进行对比:

class Snake
{
public:

    auto get_name() const -> std::string;

    auto eat(const Fruit& fruit) -> void;

    auto set_name(std::string) -> void;
};

inline auto Snake::get_name() const -> void
{ 
    return m_name;
}

inline auto Snake::eat(const Fruit& fruit) -> void
{
    ..
    // some really long definition
    ..
}

inline auto Snake::set_name(std::string) -> void
{
    m_name = std::move(name);
}

通过简单地将定义重新定位到底部并明确标记这些inline,我们可以快速了解Snake 类及其上所有相关的参与者和观察者功能,而无需在函数定义之间进行扫描。

这不会丝毫改变代码生成,但它可以通过使类自文档化来帮助提高类的可读性——如果您在团队环境中工作,或者有其他人来,这将非常有用来维护您的项目。

【讨论】:

    【解决方案3】:

    分离头文件和源文件的主要目的是减少构建依赖。如果您在头文件中定义函数,则无论何时更改该函数,您都必须重新编译使用该头文件的每个源文件。如果您在源文件中定义一个函数,那么任何时候您更改该函数都必须重新编译定义它的一个源文件。对于可能无关紧要的小型项目。对于大型项目,它显着减少了重建项目所需的时间,从而缩短了周转时间,加快了开发速度。

    所以,不,将“简单”函数放在头文件中是没有意义的。这并不能消除构建依赖。一旦一个项目安定下来,并且它的各个部分没有发生重大变化,将关键函数移动到头文件中可能是有意义的,不是为了方便,而是因为这样你可以标记它们inline,也许获得性能提升。

    【讨论】:

      猜你喜欢
      • 2020-03-03
      • 2014-03-24
      • 2013-01-28
      • 2020-03-05
      • 1970-01-01
      • 2019-04-20
      • 2018-04-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多