ADL 的陷阱是什么?

What are the pitfalls of ADL?

提问人:fredoverflow 提问时间:6/2/2010 最后编辑:fredoverflow 更新时间:11/5/2018 访问量:6334

问:

前段时间我读了一篇文章,解释了参数依赖查找的几个陷阱,但我再也找不到了。这是关于获得你不应该访问的东西或类似的东西。所以我想在这里问:ADL 的陷阱是什么?

C++ 命名空间 重载解析 argument-dependent-lookup

评论

0赞 Amir Kirsh 2/7/2021
Arthur O'Dwyer 撰写的关于 ADL 调用的歧义问题的精彩相关博客文章,如 C++17 中添加的那样,请参阅:https://quuxplusone.github.io/blog/2018/06/17/std-sizesizestd::size

答:

79赞 James McNellis 11/22/2010 #1

依赖于参数的查找存在一个巨大的问题。例如,请考虑以下实用程序:

#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()printn

实际上,事实证明,如果我们只看这段代码,我们完全不知道 会调用什么函数。它可能是此处给出的函数模板,但可能不是。为什么?与参数相关的查找。print_nprint

举个例子,假设你写了一个类来表示一只独角兽。出于某种原因,您还定义了一个名为(真是巧合!)的函数,该函数只是通过写入取消引用的空指针导致程序崩溃(谁知道你为什么要这样做,这并不重要):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_nprintprintmy_stuff::print

为什么被选中?在名称查找过程中,编译器会看到调用的参数的类型为 ,这是在命名空间中声明的类类型。my_stuff::printprintunicornmy_stuff

由于与参数相关的查找,编译器在搜索名为 的候选函数时包含此命名空间。它找到 ,然后在重载解决期间将其选为最佳可行候选函数:调用任何一个候选函数都不需要转换,并且非模板函数优先于函数模板,因此非模板函数是最佳匹配。printmy_stuff::printprintmy_stuff::print

(如果您不相信这一点,可以按原样编译此问题中的代码,并查看 ADL 的实际应用。

是的,参数相关查找是 C++ 的一个重要功能。它本质上是实现某些语言功能(如重载运算符)的所需行为所必需的(考虑流库)。也就是说,它也非常非常有缺陷,可能会导致非常丑陋的问题。已经有几个提案来修复依赖于参数的查找,但没有一个被 C++ 标准委员会接受。

评论

11赞 Chubsdad 11/22/2010
这是 ADL 的陷阱还是不谨慎使用 ADL 的陷阱?
20赞 James McNellis 11/22/2010
@Chubsdad:这是ADL的一个巨大陷阱。问题在于,您可以编写两个完全独立的库,并且不小心遇到此问题,而不知道会遇到问题。再多的“小心翼翼”也无法完全保护您免受此影响。
9赞 James McNellis 2/4/2011
@MSalters:嗯,问题在于混合库的显式语句并不总是那么明确。例如,考虑一下,如果你编写一个名为命名的空间范围函数模板,该模板合并了两个东西,并且你向它传递了两个对象。你会得到不同的结果,这取决于你是否包含(声明)。mergestd::vector<algorithm>std::merge
21赞 Luc Touraille 12/1/2011
我想说这不是一个陷阱,而是一个功能:它允许您通过提供专门针对您的类型的实现来覆盖库行为。如果没有 ADL,您将无法修改 的行为以适应您的类型。这一个广泛使用的应用是:许多标准算法需要交换值;您可以提供自己的优化版本,并且由于 ADL,它将被选中。当然,如果你能在不需要的时候防止这种覆盖,那就更好了(就像你没有被强制要求使你的成员功能虚拟一样)。printunicornswapswpa
13赞 Nawaz 10/9/2013
问题是程序员为什么要在他想调用的时候写?如果我写 ,那么我打算调用 ADL 以找到正确的重载(也可能在其他命名空间中)。如果我不想要 ADL,那么我会写.所以我不完全同意这个答案。它主要是由于缺乏有关 ADL 的基本知识而产生的。相反,我同意@LucTouraille。:-)print(x)::utility::print()print(x)::utility::print(x)
6赞 FrankHB 7/16/2018 #2

公认的答案是完全错误的 - 这不是 ADL 的错误。它显示了在日常编码中使用函数调用的粗心反模式 - 对依赖名称的无知和盲目依赖非限定的函数名称。

简而言之,如果你在函数调用中使用非限定名称,你应该承认你已经授予了函数可以在其他地方“覆盖”的能力(是的,这是一种静态多态性)。因此,C++ 中函数的非限定名称的拼写正是接口的一部分。postfix-expression

在被接受的答案的情况下,如果确实需要 ADL(即允许它被覆盖),它应该被记录下来,使用不合格作为明确的通知,因此客户将收到一份应该仔细声明的合同,不当行为将承担全部责任。否则,它是 的错误。解决方法很简单:用前缀 .这确实是 的 bug,但几乎不是语言中 ADL 规则的 bug。print_nprintprintprintmy_stuffprint_nprintutility::print_n

但是,语言规范中确实存在不需要的东西,而且从技术上讲,不仅仅是一个。它们已经实现了 10 多年,但语言中没有任何内容是固定的。它们被接受的答案遗漏了(除了最后一段到目前为止是完全正确的)。有关详细信息,请参阅本文

我可以附加一个针对名称查找讨厌的真实案例。我正在实施哪里.我发现一旦我的命名空间中有一个声明的函数模板,就不可能依赖 ADL 来实现这样的功能。这总是与在 ADL 规则下使用 ADL 的惯用语一起发现,然后会出现模板(将实例化以获得正确的)的歧义。结合 2 阶段查找规则,一旦包含包含模板的库标头,声明的顺序就不计算在内。因此,除非我使用专用函数重载所有库类型(以抑制任何候选泛型模板,以便在 ADL 之后通过重载解析来匹配),否则我无法声明模板。具有讽刺意味的是,在我的命名空间中声明的模板正是利用 ADL(考虑),它是我库中最重要的直接客户端之一(顺便说一句,不尊重异常规范)。这完美地打败了我的目的,唉......is_nothrow_swappable__cplusplus < 201703Lswapswapstd::swapusing std::swap;swapswapis_nothrow_swappablenoexcept-specificationswapswapswapswapboost::swapis_nothrow_swappableboost::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_TEMPLATEtrue

更新 2018-11-05:

啊哈,今天早上我又被ADL咬了。这一次,它甚至与函数调用无关!

今天,我正在完成将 ISO C++17 std::p olymorphic_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 实现。(免责声明:它有一些已知的错误。错误的修复将在几天内提交。

但它突然不起作用,有数百行神秘的错误消息......

错误从这一行开始。它粗略地抱怨声明不是封闭类的基。这似乎很奇怪,因为别名是用与类定义的基说明符列表中的标记完全相同的标记声明的,而且我敢肯定它们中的任何一个都不能被宏观扩展。那么为什么呢?BaseTypeMessageQueue

答案是......ADL 很烂。引入 BaseType 的行是硬编码的,名称作为模板参数,因此将根据类作用域中的 ADL 规则查找模板。因此,它发现 ,这与查找的结果不同,因为在封闭的命名空间范围内声明了实际的基类。由于使用 instance 作为默认模板参数,因此与具有 实例的实际基类的类型不同,即使在封闭的命名空间中声明也会重定向到 。通过将封闭限定作为前缀添加到 中,修复了该错误。stdstd::multimapstd::multimapstd::allocatorBaseTypepolymorphic_allocatormultimapstd::multimap=

我承认我很幸运。错误消息将问题指向此行。只有 2 个类似的问题,另一个没有任何明确的问题(我自己的问题在哪里适应 ISO C++17 的变化,而不是 C++17 之前模式中的一个问题)。我不会这么快就弄清楚这个错误是关于 ADL 的。stdstringstring_viewstd

评论

2赞 Acorn 11/5/2018
我认为现在大多数人都同意,所定义的 ADL 规则是一个错误(而不是 ADL 本身)。在您自己的命名空间(在应用程序代码中是大多数命名空间)中限定符号的所有函数调用是一件苦差事。它还损害了可读性。默认值应与此相反:显式标记哪些调用旨在执行 ADL。
0赞 FrankHB 11/5/2018
@Acorn 从POLA的角度来看,这可能更好,但如果这是真的,我怀疑会有一个专门为ADL设计的区分语法。不过,可能还有其他选择。无论如何,ADL 是翻译过程中“通常”名称查找规则的覆盖者,那么为什么不使用一些更通用的元编程工具来允许它可编程(例如卫生宏)呢?但遗憾的是,设计中没有考虑到这一点。
0赞 Weijun Zhou 11/21/2023
这就是为什么我们在 C++20 中有 CPO 的概念。std::ranges::swap