Tip of the Week #49: Argument-Dependent Lookup

2023-11-10

Tip of the Week #49: Argument-Dependent Lookup

Originally posted as totw/49 on 2013-07-14

“…whatever disappearing trail of its legalistic argle-bargle one chooses to follow…” –Antonin Scalia, U.S. v Windsor dissenting opinion

Overview

一个函数调用表达式如func(a,b,c),其中函数被命名为不使用::操作符的,这被称为未限定名称。当C++代码引用一个未限定名称的函数时,编译器会去执行匹配函数声明的搜索。令人有些人惊讶的是(与其他语言不同),除了在调用者的作用域内查找外,搜索的范围还包括了函数参数类型相关联的名称空间,这种额外的查找被称为Argument-Dependent Lookup (ADL),这绝对是在你的代码中发生的,所以你会更好的理解它是如何工作的。

Name Lookup Basics

一个函数调用必须映射到一个编译器定义的的函数。这种匹配关系的映射是在两个独立的串行处理阶段完成的,第一步就是名称解析,应用于一些作用域的搜索规则去产生一系列可以匹配的函数名,而重载决策采用该名称查找产生的重载,并尝试选择一个和调用处传入的参数最匹配的函数名称。记住这个区别,名称解析是第一步,它并不试图确定函数是否匹配,它甚至不考虑参数,它只是在作用域内搜索函数名称,重载解析本身就是一个复杂的话题,但现在不是我们的重点。只要知道它是一个单独的处理阶段,它可以从名称查找中获取输入。

当遇到一个未限定函数调用时,该函数名称可能会出现几个独立的搜索序列,每一个搜索序列都会试图匹配一系列的重载名称。最明显的搜索序列就是从调用点的作用域开始处开始向外搜索。

namespace b {
void func();
namespace internal {
void test() { func(); } // ok: finds b::func().
} // b::internal
} // b

这个名称查找与ADL无关(func没有参数),它只是简单的从函数调用处向外搜索。从本地函数作用域向外进行,到类作用域、封闭类作用域和基类,然后到命名空间作用域,并进入到封闭命名空间,最后到全局命名空间。

名称查询通过一系列日益扩大的范围进行着,只要找到具有目标名称的任何就会停止进行搜索。无论该函数的参数是否与调用点提供的函数函数兼容。当遇到包含至少一个具有目标名称的函数声明的作用域时,该作用域中的重载决策将成为该名称查找的结果。

这在下面的例子中有说明:

namespace b {
void func(const string&);  // b::func
namespace internal {
void func(int);  // b::internal::func
namespace deep {
void test() {
  string s("hello");
  func(s);  // error: finds only b::internal::func(int).
}
}  // b::internal::deep
}  // b::internal
}  // b

上面的例子会让人很困惑认为func(s)表达式会忽略掉void func(int),并继续到下一个作用域中寻找到b::func(const string&)。然而名称解析的时候并不会考虑到参数类型,它仅仅找那些函数名字叫func的并最终在b::internal这个作用域中找到并停下了。结果就是将一个明显不好的匹配交给了重载决策阶段来评估了。最终b::func(const string&)这个函数也没有出现在重载决策的阶段。

作用域搜索顺序的一个重要含义是,搜索顺序中较早出现的作用域中的重载将会隐藏后面的作用域中的重载。

Argument-Dependent Lookup

如果一个函数调用传递了函数那么几个并行的名称查找过程都会同时进行,这些额外的查找是从这个函数的每一个调用参数所在的命名空间中开始。当遇到名称匹配的作用域时并不会停止查找,只有遇到匹配的那一个才会结束。

The Simple Case

考虑下面这段代码:

namespace aspace {
struct A {};
void func(const A&);  // found by ADL name lookup on 'a'.
}  // namespace aspace

namespace bspace {
void func(int);  // found by lexical scope name lookup
void test() {
  aspace::A a;
  func(a);  // aspace::func(const aspace::A&)
}
}  // namespace bspace

上面的代码中在调用func(a)时会存在两个名称查找的过程,一个是从bspace::test()所在的作用域开始向外进行名称的查找,当没有发现有任何匹配的名称时就开始在bspace所在作用域中进行查找,并发现了func(int)于是停止了查找。另外一个名称查找的过程是ADL,它是从函数的调用参数a所在的namespace开始,这上面的例子中就是aspace,在这个namespace中找到了aspace::func(const aspace::A&)并停止。最后将这两个匹配到的函函数交给重载决策,在重载决策阶段会根据参数进行最佳匹配,最后寻找到的最佳匹配就是aspace::func(const aspace::A&),而bspace::func(int) 在重载阶段发现其参数并不匹配所以被拒绝了。

基于调用处所在作用域开始的名称查找和每个因为ADL所触发的名称查找可以被认为是并行发生的,每一个搜索都会返回一组候选的函数重载。所有的这些搜索所产出的结果都会放在一个集合中,最后通过重载决策阶段以确定最佳的匹配。如果有一批最佳的匹配,那么编译器就会发出一个模糊的错误 “只能有一个最佳的匹配”,如果没有找到任何一个最佳的匹配,编译器也会报出一个错误 “必须要有一个匹配”。

Type-Associated Namespaces

前面的例子是一个比较简单的例子,更复杂的类型可以有多个与之关联的namespace,这个与之关联的namespace包括了与这个类型相关的任何namespace,参数类型的全称所在的namespace就是其中的一部分,此外还有模版参数的类型所在的namespace,还包括了其直接或间接的父类所在的namespace。例如一个单参数的函数调用a::A<b::B, c::internal::C*>将会产生abc::internal等三个搜索的域,在每一个搜索的域中都会查找和调用函数名称相同的函数。下面这个例子就显示了这些效果:

namespace aspace {
struct A {};
template <typename T> struct AGeneric {};
void func(const A&);
template <typename T> void find_me(const T&);
}  // namespace aspace

namespace bspace {
typedef aspace::A AliasForA;
struct B : aspace::A {};
template <typename T> struct BGeneric {};
void test() {
  // ok: base class namespace searched.
  func(B());
  // ok: template parameter namespace searched.
  find_me(BGeneric<aspace::A>());
  // ok: template namespace searched.
  find_me(aspace::AGeneric<int>());
}
}  // namespace bspace

Tips

随着基本的名称查找机制在你的脑海中开始记忆犹新后,请考虑下面这些Tips,这些Tips可能会帮助您写出更佳的C++代码。

Type Aliases

有的时候要确定一个类型所关联的namespace是需要花一些时间来辨别的。typedefusing声明可以给一个类型引入别名。在这些情况下,选择要搜索的namespace列表之前需要将这些别名进行解析,并扩展为他们的源类型。这是typedefusing声明可能会带来的一些误导,因为他们可能会导致您在对ADL需要搜索哪些namespace时会进行不正确的预测。如下所示:

namespace cspace {
// ok: note that this searches aspace, not bspace.
void test() {
  func(bspace::AliasForA());
}
}  // namespace cspace

Caveat Iterator

对迭代器要小心。你不知道他们关联的是什么namespace,所以不要依赖于ADL来解决涉及迭代器的函数调用的名称解析。它们可能只是指向元素的指针,或者可能是在一个与容器实现所在的namespace无关的一个私有namespace中。

namespace d {
int test() {
  std::vector<int> vec(a);
  // maybe this compiles, maybe not!
  return count(vec.begin(), vec.end(), 0);
}
}  // namespace d

上面的代码依赖于std::vector<int>::iteratorint*(这是可能的),还是某个类型在具有count重载函数的命名空间中(如std::count())。这可能会在某些平台上是可以运行的,而在其他平台上则无法运行,或者它可以在debug下可以正常工作,而在release下无法运行。这种情况下使用带有限定名称的函数调用方式会更好,例如可以像这样来调用count函数,std::cout()

Overloaded Operators

运算符(例如 +<< )可以被认为是一种函数名称,例如operator+(a,b)operator<<(a,b)这些都被认为是未限定名称的调用。ADL最重要的应用就是用于在通过operator<<来记录日志的时候。通常我们会看到像std::cout << obj;这样的调用,对于obj我们假定认为它的类型是O::Obj。这个语句展开来看就是这样的operator<<(std::ostream&, const O::Obj&),是一个未限定名称的函数调用,它将会通过ADLstd::ostream参数所对应的std namespace中进行名称的查找,以及第二个参数0::0bj0 namespace中进行查找,当然还会从函数调用处所在的作用域开始向外进行查找。

将这些运算符放在与他们要操作的用户自定义类型相同的名称空间中很重要:在上面的namespace 0的例子中,如果将operator<<放在像::(全局namespace)这样的外部namespace中,该操作符将工作一段时间,直到有人非常无辜的将一个不相干的其他类型的operator<<操作符放在namespace 0

Fundamental Types

请注意,基本类型(如intdouble等)不和全局namespace关联。它们不关联任何namespace,也不会对ADL产生作用,指针和数组类型和他们所指向的对象和或元素类型所在的namespace进行关联。

Refactoring Gotchas

如果将参数类型更改为非限定函数调用的话,会影响那些具有重载的函数调用行为。只是将一个类型移动到namespace中,并在旧的命名空间中使用typedef以实现兼容性,这是没有帮助的,实际上只会使问题更难被诊断。将类型移动到新的命名空间中时要小心。

类似的,将函数移动到新的namespace中,并设置好using声明。这意味着非限定的调用可能将不会找到他,可悲的是,他们仍然可以通过找到不同的函数重载来完成编译。当移动一个函数到新的namespace中时要小心。

Final Thought

相对较少的程序员理解与函数查找相关的确切规则和极端情况,该语言规范里面包含了13页关于名称搜索包含了哪些规则、特殊情况、以及和友元函数闭包类搜索范围等,需要你额外的小心。尽管存在这些复杂性,但如果您始终牢记并行名称搜索的基本概念,那么您将有足够的基础来理解函数调用和运算符是如何解析的。现在通过本文,您将能够理解函数调用和运算符是如何选择函数声明的,并且当发生重载决议失败和名称覆盖等令人费解的构建错误时,你将更容易诊断。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Tip of the Week #49: Argument-Dependent Lookup 的相关文章

随机推荐

  • 基于YOLOv5的血细胞识别和计数

    VOC格式标注转为yolov5格式 原数据格式是xml文件对目标细胞注释 现在需要将这种注释转换为yolov5所需的格式 即每个图像对应一个txt文件 文件中存储该图像中全部细胞的类别和坐标 一行存储一个细胞的信息 如下图 编写脚本进行注释
  • [Unity]各种Debug方法笔记

    无论是萌新还是Dalao 遇到Bug总是难免的 拒绝反驳 所以一些好的Debug方法就显得尤为重要 这篇文章既写给自己 也给看到文章的大家一个参考 内容主 quan 要 bu 是脚本的Debug方法 ps 如有出错漏记得以我能看到的方式指出
  • COCO数据处理(二)根据自己提取的类的json文件生成对应的mask二值图并画在原图上

    文章目录 COCO数据集根据json文件生成mask二值图 文件目录 目录说明 代码 一 生成mask图 代码 二 将mask图画在原图上 效果图 COCO数据集根据json文件生成mask二值图 文件目录 目录说明 data coco a
  • java中JDBC当中请给出一个DataSource的HelloWorld例子

    马克 to win 在前面 的jdbc的Helloworld程序当中 我们用DriverManager来获取数据库连接 事实上通过这种方法获取数据库连接 是比较耗费计算机资 源的 当然了 这也是没有办法的事儿 就像我们买贵书必须花大价钱一样
  • 【Android】App开发-布局篇

    UI的开发离不开各个组件的精准布局 在我们学习了控件之后 控件篇 我们就需要对这些控件进一一排布 让它们在各个指定的位置 目录 LinearLayout线性布局 RelativeLayout布局 FrameLayout布局 TableLay
  • 【Python爬虫】将爬下来的数据保存到redis数据库里面

    redis库中的Redis类对Hash数据类型操作的常用方法 方法名 具体说明 hset name key value 哈希中添加一个键值对 hmset name mapping 设置哈希中的多个键值对 hmget name keys ar
  • 逻辑架构和物理架构

    逻辑架构和物理架构 理论上划分了5种架构视图 分别是 逻辑架构 开发架构 运行架构 物理架构 数据架构 逻辑架构 逻辑架构关注的是功能 包含用户直接可见的功能 还有系统中隐含的功能 或者更加通俗来描述 逻辑架构更偏向我们日常所理解的 分层
  • HTML学习(二)HTML基础

    以这个为例 h1 我的第一个标题 h1 p 我的第一个段落 p DOCTYPE 前用来申明这是一个html 这里的html不区分大小写 HTML标题 HTML 标题 Heading 是通过 h1 h6 标签来定义的 h1 1级标题 h1 H
  • R语言优雅地修改列名称

    R语言优雅地修改列名称 在R语言中 修改数据框 DataFrame 或矩阵 Matrix 的列名称是一项常见的任务 通过优雅地修改列名称 可以提高代码的可读性和可维护性 在本文中 我将介绍几种优雅的方法来修改列名称 并提供相应的源代码示例
  • GPU计算

    文章目录 GPU计算 1 GPU和CPU的区别 2 GPU的主要参数解读 3 如何在pytorch中使用GPU 4 市面上主流GPU的选择 GPU计算 1 GPU和CPU的区别 设计目标不同 CPU基于低延时 GPU基于高吞吐 CPU 处理
  • 95-34-025-Context-AbstractChannelHandlerContext

    文章目录 1 概述 2 继承体系 3 类签名 4 关键字段 5 构造方法 6 ChannelRead事件 6 1 findContextInbound 7 invokeHandler 1 概述 2 继承体系
  • tensorrt转换模型进行了哪些操作

    对于网络layer graph进行的操作 消除输出未使用的层 消除相当于无操作的操作 卷积 偏置和ReLU运算的融合 具有足够相似参数和相同源张量的运算聚合 例如 GoogleNet v5的初始模块中的1x1卷积 inception结构中同
  • DW9718AF.c

    Copyright C 2015 MediaTek Inc This program is free software you can redistribute it and or modify it under the terms of
  • ERP系统总体解决方案 附下载地址

    企业资源计划即 ERP Enterprise Resource Planning 由美国 Gartner Group 公司于1990年提出 企业资源计划是 MRP II 企业制造资源计划 下一代的制造业系统和资源计划软件 除了MRP II
  • 大数据学习框架及指南

    Hadoop生态圈 一 采集 数据从哪里来 主要包括flume等 一 存储 海量的数据怎样有效的存储 主要包括hdfs Kafka 二 计算 海量的数据怎样快速计算 主要包括MapReduce Spark storm等 三 查询 海量数据怎
  • 数据结构与算法笔记2(线性表)

    1 线性表 1 1线性表是一种逻辑关系 见绪论 1 2定义 是具有相同类型的n个元素的有限序列 其中n为表长 n 0时为空表 关键词 相同类型 一般处理的数据元素都是相同类型 比如一个人那么都是人 而不会把人与车放在一起 关键词 有限序列
  • Java泛型知识点整理

    Java泛型知识点整理 Java泛型 泛型提供了编译时类型安全检测机制 该机制允许程序员在编译时检测到非法的类型 泛型的本质是参数化类型 也就是说所操作的数据类型被指定为一个参数 比如我们要写一个排序方法 能够对整型数组 字符串数组甚至其他
  • ConcurrentHashMap为什么是线程安全的?

    1 ConcurrentHashMap的原理和结构 我们都知道Hash表的结构是数组加链表 就是一个数组中 每一个元素都是一个链表 有时候也把会形象的把数组中的每个元素称为一个 桶 在插入元素的时候 首先通过对传入的键 key 进行一个哈希
  • MapReduce的Job提交流程

    编写一个简单的WordCount程序 Mapper import org apache hadoop io LongWritable import org apache hadoop io Text import org apache ha
  • Tip of the Week #49: Argument-Dependent Lookup

    Tip of the Week 49 Argument Dependent Lookup Originally posted as totw 49 on 2013 07 14 whatever disappearing trail of i