前段时间我读了一篇文章解释了参数依赖查找的几个缺陷,但我再也找不到了。它是关于获取您不应该访问的东西或类似的东西。所以我想我会在这里问:ADL的缺陷是什么?
答案 0 :(得分:64)
依赖于参数的查找存在一个巨大的问题。例如,考虑以下实用程序:
#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
的调用的参数是unicorn
类型,它是在名称空间my_stuff
中声明的类类型。
由于依赖于参数的查找,编译器在搜索名为print
的候选函数时包含此命名空间。它找到my_stuff::print
,然后在重载解析期间选择它作为最佳可行候选者:调用任一候选print
函数不需要转换,并且非模板函数优先于函数模板,因此非模板函数my_stuff::print
是最佳匹配。
(如果您不相信这一点,您可以按原样编译此问题中的代码并查看ADL的实际操作。)
是的,依赖于参数的查找是C ++的一个重要特性。实际上,需要实现某些语言功能(如重载运算符)的所需行为(考虑流库)。也就是说,它也非常非常有缺陷,可能导致非常丑陋的问题。有几个建议要修复依赖于参数的查找,但是C ++标准委员会都没有接受它们。
答案 1 :(得分:2)
公认的答案完全是错误的-这不是ADL的错误。它显示了一个粗心的反模式,该模式在日常编码中使用函数调用-对依赖名称的无知,并盲目地依赖不合格的函数名称。
简而言之,如果您在函数调用的postfix-expression
中使用了不合格的名称,则您应该承认您已授予可以在其他位置“覆盖”该功能的能力。 (是的,这是一种静态多态性)。因此,C ++中函数的不合格名称的拼写恰好是接口的一部分。
在接受答案的情况下,如果print_n
确实需要ADL print
(即允许它被覆盖),则应该使用不合格的print
对其进行记录作为明确的通知,客户将收到一份合同,约定print
应当经过仔细声明,并且不当行为将是my_stuff
的全部责任。否则,它是print_n
的错误。解决方法很简单:用前缀print
限定utility::
。这确实是print_n
的错误,但几乎不是该语言中ADL规则的错误。
但是,在语言规范中,存在要做不需要的东西,从技术上讲,不是只有一个。他们已经实现了10多年,但是该语言的任何内容都尚未确定。他们被所接受的答案错过了(除非最后一段到目前为止是唯一正确的)。有关详情,请参见此paper。
我可以针对姓名查找讨厌添加一个实际案例。我正在实现is_nothrow_swappable
,其中__cplusplus < 201703L
。我发现一旦我在命名空间中有一个声明的swap
函数模板,就不可能依靠ADL来实现这种功能。这样的swap
总是会和惯用std::swap
引入的using std::swap;
一起使用,以根据ADL规则使用ADL,然后swap
就会变得模棱两可,其中{{1 }}模板(将实例化swap
以获得正确的is_nothrow_swappable
)被调用。一旦包含了noexcept-specification
模板的库头,便与两阶段查找规则结合使用,声明的顺序不计算在内。因此,除非我使用专门的swap
函数将所有库类型的 all 重载(以抑制ADL之后通过重载分辨率匹配的任何候选通用模板swap
),否则无法声明该模板。具有讽刺意味的是,在我的命名空间中声明的swap
模板正是利用ADL(考虑swap
),并且它是我的库中boost::swap
的最重要的直接客户端之一(BTW,{{ 1}}不遵守异常规范。这完全打败了我的目的,感叹……
is_nothrow_swappable
尝试https://wandbox.org/permlink/4pcqdx0yYnhhrASi,然后将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
}
,以查看歧义。
更新2018-11-05:
啊哈,今天早上我又被ADL咬伤了。这次甚至与函数调用无关!
今天,我正在完成将ISO C++17 std::polymorphic_allocator
移植到我的代码库的工作。由于一些容器类模板是很早以前在我的代码中引入的(例如this),所以这次我只用别名模板替换声明,例如:
USE_MY_SWAP_TEMPLATE
...,因此默认情况下可以使用my implementation of polymorphic_allocator
。 (免责声明:它具有一些已知的错误。这些错误的修复将在几天内提交。)
但是突然出现了数百行的隐式错误消息,它却无法正常工作...
错误从this line开始。它大致抱怨声明的true
不是封闭类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>;
的基础。这似乎很奇怪,因为使用与类定义的 base-specifier-list 中相同的标记声明了别名,并且我敢肯定它们中的任何一个都不能宏扩展。那为什么呢?
答案是... ADL很烂。 The line inroducing BaseType
以BaseType
名称作为模板参数进行了硬编码,因此将根据ADL规则在类范围内查找该模板。因此,它找到MessageQueue
,这与查找结果不同,因为在封闭的名称空间范围中,实际的基类声明为。由于std
使用std::multimap
实例作为默认模板参数,因此std::multimap
与具有实例std::allocator
甚至{{1}的实例的实际基类的类型不同封闭的名称空间中声明的}重定向到BaseType
。通过将封闭条件作为polymorphic_allocator
的前缀权限添加,可以修复该错误。
我承认我很幸运。错误消息将问题引导至此行。只有两个类似的问题,the other没有任何明确的multimap
(其中std::multimap
是my own one适用于ISO C ++ 17的=
更改,不是std
(在C ++ 17之前的模式中)。我不会这么快就发现关于ADL的错误。