【问题标题】:Where to put function declaration?在哪里放置函数声明?
【发布时间】:2020-05-15 05:11:56
【问题描述】:

假设我正在开发一个日志记录功能。在logging.h 里面我声明了稍后应用程序使用的函数。

// logging.h

#include <string>

namespace logging {

void LogThis(const std::string& text);

};  // namespace logging

它的定义明显在logging.cpp里面:

// logging.cpp

void logging::LogThis(const std::string& text) {
  std::cout << "Log: " << text << '\n';
}

现在让我们假设我的LogThis 函数的工作被拆分为一些较小的辅助函数。它们不是日志界面的一部分。我们以Prettify函数为例。

// logging.cpp

void logging::LogThis(const std::string& text) {
  Prettify(text);
  std::cout << "Log: " << text << '\n';
}

我的问题是:Prettify的函数声明应该放在哪里?我不应该将它包含在logging.h头文件中,因为那样它可以被其他编译单元调用它不是界面的一部分。所以把它放在logging.cpp 里面而不是这样?

// logging.cpp

namespace logging {

void Prettify(std::string& ugly_text);

void LogThis(const std::string& text) {
  Prettify(text);
  std::cout << "Log: " << text << '\n';
}

void Prettify(std::string& ugly_text) {
  // making it pretty...
}

}

我正在寻找一些最佳实践/经验法则/对此的意见 :) 提前致谢!

【问题讨论】:

  • 其他翻译单位想用吗?
  • 不,Prettify 仅由该日志翻译单元使用。
  • 把它放在 logging.cpp 中的匿名命名空间中
  • @JesperJuhl 放入匿名命名空间而不是 logging 命名空间有什么好处?
  • 旁注:如果将Prettify 的定义放在LogThis 的定义之上,则无需单独声明。

标签: c++ function coding-style encapsulation


【解决方案1】:

对于文件中需要的东西,我只需将其放置在 C++ 文件本身的匿名命名空间中,类似于函数上的旧 C static 关键字的现代等价物(a):

namespace {
    void WeaveMagic(std::string& ugly_text) {
        WeaveMoreMagic(ugly_text);
    }
    void Prettify(std::string& ugly_text) {
        WeaveMagic(ugly_text);
    }
}

如果您将其放在函数的任何使用之前,并确保调用的严格层次结构,您可以跳过声明,因为定义提供了所需的信息,如上所示。

当然,如果多个匿名函数之间存在任何循环依赖关系(即循环递归),您仍然需要提供声明:

#include <iostream>

namespace {
    int DivThree(int val); // needed to implement AddOne()

    int AddOne(int val) {
        std::cout << "AddOne " << val << " -> " << (val + 1) << '\n';
        if (val > 0) return DivThree(val + 1);
        return val;
    }

    int DivThree(int val) {
        std::cout << "DivThree " << val << " -> " << (val / 3) << '\n';
        return AddOne(val / 3);
    }
}

int main(){
    int final = AddOne(18);
    std::cout << "Final " << final << '\n';
    return 0;
}

而且,是的,这是 非常 做作的,但是循环递归的好例子很少而且相差甚远 :-) 输出是:

AddOne 18 -> 19
DivThree 19 -> 6
AddOne 6 -> 7
DivThree 7 -> 2
AddOne 2 -> 3
DivThree 3 -> 1
AddOne 1 -> 2
DivThree 2 -> 0
AddOne 0 -> 1
Final 0

(a) CPP 核心指南 SF.22 实际上涵盖了这一点:

为所有内部/非导出实体使用未命名(匿名)命名空间。

原因:没有任何外部可以依赖于嵌套未命名命名空间中的实体。考虑将每个定义放在未命名命名空间中的实现源文件中,除非这是定义“外部/导出”实体。

API 类及其成员不能存在于未命名的命名空间中;但在实现源文件中定义的任何“帮助”类或函数都应位于未命名的命名空间范围内。

【讨论】:

  • 重要的是要理解,这给出了内部链接的名称,与 OP 的代码相比,它给出了外部链接(因此可能导致与其他一些外部链接符号 Prettify 的 ODR 冲突)
  • 我刚刚注意到甚至有一个 Cpp 核心指南:SF.22
  • @paxdiablo 假设我有 4-5 个辅助函数。我会打开一个匿名命名空间,然后编写 4-5 个函数声明,然后是 4-5 个函数定义吗?
  • @Lexusminds:不,只需将匿名名称空间放在顶部即可。我通常把它放在包含和外部可见的代码之间。如果你这样做,定义会提供声明。
  • @paxdiablo 如果帮助函数可能会相互调用,那将不起作用,对吧?那么顺序很重要或者需要函数声明。
【解决方案2】:

如果您只对函数进行操作,正如@paxdiablo 所写,您可以使用匿名命名空间(查看他的答案)。

我有一些基于 C 的习惯,所以我个人也将其视为static 函数。但我不确定 C++ *者会如何看待它 :)。 static(在此上下文中)使编译单元 (logging.cpp) 的函数成为本地函数,因此无法从外部链接。

//logging.cpp

static void Prettify(std::string& ugly);

void LogThis(const std::string& text) {
  Prettify(text);
  std::cout << "Log: " << text << '\n';
}

static void Prettify(std::string& ugly) { }

但是,如果您的日志记录实用程序是面向对象的。我建议你使用 D-pointer 和 Q-Pointer 设计模式(也称为 PImpl idiom)-https://en.cppreference.com/w/cpp/language/pimpl

//logging.h
#include <string>

class loggingImpl;

class logging {
public :
   logging();
   virtual ~logging();
   void LogThis(const std::string& text);

protected :
   loggingImpl *impl;
};

//logging.cpp
class loggingImpl
{
public :
   loggingImpl(logging *p) : qptr(p) { }

   void Prettify(std::string& ugly) { }
   //anything what you need and should be hided
   // access parent through qptr
protected :
   logging *qptr;
};

logging::logging() : impl(new loggingImpl) { }

logging::~logging() { delete impl; }

void logging::LogThis(const std::string& text) {
  impl->Prettify(text);
  std::cout << "Log: " << text << '\n';
}

正如您所写,由于限制未使用符号的可见性,将声明放在头文件中是不合适的。

【讨论】:

  • 我们是*分子,我们将如何看待它应该是显而易见的:-)