前段时间我读了一篇文章,解释了参数依赖查找的几个陷阱,但我再也找不到了。这是关于获得对你不应该访问的东西的访问权或类似的东西。所以我想我会在这里问:ADL 的陷阱是什么?
2 回答
依赖于参数的查找存在一个巨大的问题。例如,考虑以下实用程序:
#include <iostream>
namespace utility
{
template <typename T>
void print(T x)
{
std::cout << x << std::endl;
}
template <typename T>
void print_n(T x, unsigned n)
{
for (unsigned i = 0; i < n; ++i)
print(x);
}
}
这很简单,对吧?我们可以调用print_n()
并传递任何对象,它会调用print
以打印对象n
时间。
实际上,如果我们只看这段代码,我们完全不知道.会调用什么函数print_n
。它可能是print
这里给出的函数模板,但也可能不是。为什么?依赖于参数的查找。
例如,假设您编写了一个类来表示独角兽。出于某种原因,您还定义了一个名为print
(多么巧合!)的函数,它只是通过写入一个取消引用的空指针而导致程序崩溃(谁知道您为什么这样做;这并不重要):
namespace my_stuff
{
struct unicorn { /* unicorn stuff goes here */ };
std::ostream& operator<<(std::ostream& os, unicorn x) { return os; }
// Don't ever call this! It just crashes! I don't know why I wrote it!
void print(unicorn) { *(int*)0 = 42; }
}
接下来,您编写一个创建独角兽并打印四次的小程序:
int main()
{
my_stuff::unicorn x;
utility::print_n(x, 4);
}
你编译这个程序,运行它,然后……它崩溃了。“什么?!不可能,”你说:“我刚刚调用print_n
了 ,它调用了print
打印独角兽四次的函数!” 是的,这是真的,但它没有调用print
你期望它调用的函数。它被称为my_stuff::print
。
为什么被my_stuff::print
选中?在名称查找期间,编译器看到调用的参数print
是 type unicorn
,这是在命名空间中声明的类类型my_stuff
。
由于依赖于参数的查找,编译器在搜索名为print
. 它找到my_stuff::print
,然后在重载决策期间将其选为最佳可行候选:调用任一候选print
函数都不需要转换,并且非模板函数比函数模板更受青睐,因此非模板函数my_stuff::print
是最佳匹配。
(如果您不相信这一点,您可以按原样编译此问题中的代码并查看 ADL 的实际效果。)
是的,依赖于参数的查找是 C++ 的一个重要特性。它本质上是实现某些语言特性的所需行为,如重载运算符(考虑流库)。也就是说,它也非常非常有缺陷,可能会导致非常丑陋的问题。已经有几个建议来修复依赖于参数的查找,但没有一个被 C++ 标准委员会接受。
公认的答案是完全错误的——这不是 ADL 的错误。它显示了在日常编码中使用函数调用的粗心反模式——忽略依赖名称并盲目依赖不合格的函数名称。
简而言之,如果您在postfix-expression
函数调用中使用非限定名称,您应该承认您已授予该函数可以在其他地方“覆盖”的能力(是的,这是一种静态多态性)。因此,C++ 中函数的非限定名称的拼写正是interface的一部分。
在接受的答案的情况下,如果print_n
确实需要 ADL print
(即允许它被覆盖),则应该使用 unqualifiedprint
作为显式通知来记录它,因此客户将收到一份print
应仔细声明的合同,并且不当行为将是所有的责任my_stuff
。否则,它是一个错误print_n
。修复很简单:print
使用前缀限定utility::
。这确实是一个错误print_n
,但几乎不是语言中 ADL 规则的错误。
但是,语言规范中确实存在不需要的东西,而且从技术上讲,不仅仅是一个. 它们实现了 10 多年,但语言中的任何内容都没有固定下来。接受的答案错过了它们(除了最后一段直到现在才完全正确)。有关详细信息,请参阅本文。
我可以针对讨厌的名称查找附加一个真实案例。我正在实施is_nothrow_swappable
where __cplusplus < 201703L
。swap
一旦我的命名空间中有一个声明的函数模板,我发现不可能依靠 ADL 来实现这样的功能。这种情况总是会与在 ADL 规则下使用 ADL的惯用语swap
一起发现,然后在调用模板(将实例化以获得正确的)的位置时会出现歧义。结合两阶段查找规则,一旦包含模板的库头被包含在内,声明的顺序就不会计算在内。所以,除非我用专门的重载我所有的库类型std::swap
using std::swap;
swap
swap
is_nothrow_swappable
noexcept-specification
swap
swap
函数(以抑制swap
在 ADL 之后通过重载解析匹配的任何候选通用模板),我无法声明模板。具有讽刺意味的是,swap
在我的命名空间中声明的模板正是使用 ADL(考虑boost::swap
),它是我的库中最重要的直接客户端之一is_nothrow_swappable
(顺便说一句,boost::swap
不尊重异常规范)。这完全打破了我的目的,叹息......
#include <type_traits>
#include <utility>
#include <memory>
#include <iterator>
namespace my
{
#define USE_MY_SWAP_TEMPLATE true
#define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false
namespace details
{
using ::std::swap;
template<typename T>
struct is_nothrow_swappable
: std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))>
{};
} // namespace details
using details::is_nothrow_swappable;
#if USE_MY_SWAP_TEMPLATE
template<typename T>
void
swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::value)
{
// XXX: Nasty but clever hack?
std::iter_swap(std::addressof(x), std::addressof(y));
}
#endif
class C
{};
// Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library?
#if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE
void
swap(C&, C&) noexcept
{}
#endif
} // namespace my
int
main()
{
my::C a, b;
#if USE_MY_SWAP_TEMPLATE
my::swap(a, b); // Even no ADL here...
#else
using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone.
swap(a, b); // ADL rocks?
#endif
}
尝试https://wandbox.org/permlink/4pcqdx0yYnhhrASi并转向USE_MY_SWAP_TEMPLATE
查看true
歧义。
2018 年 11 月 5 日更新:
啊哈,今天早上我又被ADL咬了。这次它甚至与函数调用无关!
今天我完成了将ISO C++17std::polymorphic_allocator
移植到我的代码库的工作。由于很久以前在我的代码中引入了一些容器类模板(像这样),这次我只是将声明替换为别名模板,例如:
namespace pmr = ystdex::pmr;
template<typename _tKey, typename _tMapped, typename _fComp
= ystdex::less<_tKey>, class _tAlloc
= pmr::polymorphic_allocator<std::pair<const _tKey, _tMapped>>>
using multimap = std::multimap<_tKey, _tMapped, _fComp, _tAlloc>;
...所以它可以默认使用我的实现polymorphic_allocator
。(免责声明:它有一些已知的错误。错误的修复将在几天内提交。)
但它突然不起作用,有数百行神秘的错误消息......
错误从这一行开始。它大致抱怨声明BaseType
的不是封闭类的基础MessageQueue
。这看起来很奇怪,因为别名的声明与类定义的基本说明符列表中的标记完全相同,而且我确信它们中的任何一个都不能进行宏扩展。所以为什么?
答案是……ADL 很烂。行 inroducingBaseType
使用名称作为模板参数进行硬编码,因此将根据类范围内的std
ADL 规则查找模板。因此,它发现与在封闭命名空间范围中声明的实际基类中查找的结果不同。由于使用实例作为默认模板参数,因此与具有实例的实际基类的类型不同,即使在封闭命名空间中声明也被重定向到。通过将封闭的限定条件添加为 的前缀,修复了该错误。std::multimap
std::multimap
std::allocator
BaseType
polymorphic_allocator
multimap
std::multimap
=
我承认我很幸运。错误消息将问题指向这一行。只有 2 个类似的问题,另一个没有任何明确的问题std
(我自己的string
问题在哪里适应 ISO C++17 的变化,而不是C++17 之前的模式)。我不会这么快就发现这个错误是关于 ADL 的。string_view
std