返回资讯中心
外部精选
软件工程
#开发者实时流

C++ 调用能看见哪些函数:命名空间遮蔽如何改变重载解析

来源标题:Which functions a C++ call can see

一篇 Modern C++ Fundamentals 系列文章用实例说明:当另一个团队在 analytics 命名空间内新增 util 命名空间后,原本限定调用的 util::to_percent() 会解析到新函数,导致输出从 12.50% 变为 1250.0%。文章解释了 C++ 名字查找与重载解析的规则。

Hacker News 最新ibobev2026/11/2 · 昨天27 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

这是 Modern C++ Fundamentals 系列的第 6 篇,专门讲那些经验丰富的开发者也会悄悄搞错的、承载关键语义的 C++ 规则。本篇承接上一篇《一个硬编码的 C++ 标准如何破坏了单一定义规则》。

一个报表模块用基点(basis points)保存流失率,并用公司通用工具库里的 util::to_percent() 来格式化它。这个调用是限定调用,很长一段时间里它都输出 churn: 12.50%。后来另一个团队在 analytics 内部添加了自己的 util 命名空间,位置在一份只有间接包含关系的头文件里:

#include <format> #include <print> #include <string>

namespace util { // the company-wide utility library inline std::string to_percent(int basis_points) return std::format("{:.2f}%", basis_points / 100.0); } // namespace util

namespace analytics { namespace util { // added later by another team inline std::string to_percent(double ratio) return std::format("{:.1f}%", ratio * 100); } // namespace util

namespace reports { std::string churn_line(int churn_bp) return "churn: " + util::to_percent(churn_bp); } // namespace reports } // namespace analytics

int main() std::println("{}", analytics::reports::churn_line(1250));

现在程序输出的是 churn: 125000.0%。1250 个基点被转换成了 double 类型的 1250.0,然后被交给了一个期望收到比值的函数,而 ::util::to_percent(int) 这个精确匹配根本没有被考虑。GCC 15 和 Clang 21 编译这段代码时一声不吭,即使是在 -Wall -Wextra -Wpedantic 之外再加上 -Wconversion 也一样。-Wconversion 只对可能改变值的转换发出警告,而把 32 位 int 转成 double 不会改变值。

Titus Winters 在 2017 年的 Abseil Tip of the Week #130 中描述过这种失败:

Titus Winters, Abseil Tip of the Week #130: Namespace Naming

这意味着,如果你的任何传递性包含(transitive include)引入了一个此前未使用过的命名空间,而它与你在某个未限定命名空间中所用任何符号的前导命名空间匹配,你的构建就可能被破坏。严格来说,这甚至不一定会导致构建失败:如果有人添加了名称匹配、API 在语法上兼容的东西,那么这个 API 的实现可能完全不兼容,从而在运行时造成大范围的混乱。

同一条提示给出了原因:“Names aren’t chosen to be unique.”(名字并不是为了唯一而挑选的。)

每个调用都经过两个主要步骤:名字查找收集候选函数,重载决议挑出最佳候选。在这里,名字查找只给重载决议提供了一个候选 analytics::util::to_percent(double),而 ::util::to_percent(int) 从未到达重载决议这一步。本文和下一篇讲名字查找:本篇讲普通查找,下一篇讲参数相关查找。重载决议放在第三篇。免费的自测题对此做了检验。它的“Names and language plumbing”类别涵盖命名空间、名字查找、定制点和重载决议,外加保留标识符和 ODR;在 2026 年 9 月 25 日的一次快照中,该类别在 199 次完成中平均正确率为 35.7%。

出现在 :: 右侧的名字是限定的:util::to_percent 里的 to_percent,或者 std::swap 里的 swap。限定查找很容易推理,因为它只搜索你指名的那个作用域,就像文件系统中的路径([basic.lookup.qual])。对于命名空间,搜索还会覆盖它的内联命名空间。它通过 using 指令引入的命名空间只在找不到时才作为回退被搜索,即当该命名空间及其内联命名空间都没有声明该名字时。对于类,搜索还会覆盖它的基类。

前面没有 :: 的名字是未限定的。这包括第一个 :: 左侧的名字,比如 util::to_percent 里的 util。未限定查找更难推理,因为有多条规则决定编译器在哪里找到名字。尽管如此,我们大多数时候写的还是未限定名字:因为我们懒。未限定名字就像相对路径:在你弄清楚自己处在哪个目录之前,你无法判断它指向什么。未限定查找([basic.lookup.unqual])从内向外搜索调用周围的作用域:代码块、外层代码块、如果代码在成员函数里则还有类和它的基类,然后是每一层外层命名空间直到全局命名空间。它停在第一个包含该名字任何声明的那个作用域。那个声明是不是函数、能不能接受这些参数,都不重要。

global namespace namespace analytics namespace reports body of churn_line util::to_percent(int) exact match, not reached util::to_percent(double) util found here, the search stops util::to_percent(churn_bp) 1 no util 2 no util 3 reached directly by ::util::to_percent

在 churn_line 里,对 util 的搜索从 churn_line 的函数体开始,向外到 reports,再到 analytics,找到了 analytics::util,于是停下。候选集就是那个命名空间里的 to_percent。所以一行修复方案就是 ::util::to_percent(churn_bp):前导 :: 指定全局命名空间,把相对路径变成绝对路径,搜索就绝不会进到 analytics 里面。不过没人愿意在来自另一个命名空间的每个名字前都加上 ::。Titus 在同一条提示里把这种做法列为选项之一,并称之为“very verbose and sort of weird”(非常啰嗦,而且有点怪)。

对于你所调用的名字,一个同名的变量或类型会和函数一样让搜索停下。这也是深层嵌套命名空间成为负担的原因:每一层都是另一个作用域,一个不相关的声明就能让搜索在那里停下。我的工作坊幻灯片建议保持命名空间扁平,或者至少浅一些,以防意外的查找问题。这是 C++,不是 Java ;-)

成员函数遵循同样的规则,只是派生类会先于它的基类被搜索([class.member.lookup]):

#include <print>

struct base_logger { void write(double value) std::println("base_logger::write(double): {}", value); };

struct file_logger : base_logger { void write(int error_code) std::println("file_logger::write(int): {}", error_code); };

int main() file_logger log;

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →