ADL的缺陷是什么?

fre*_*low 50 c++ namespaces overload-resolution argument-dependent-lookup

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

Jam*_*lis 65

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

#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这很简单,对吧?我们可以调用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; }
}
Run Code Online (Sandbox Code Playgroud)

接下来,您编写一个创建独角兽的小程序并将其打印四次:

int main()
{
    my_stuff::unicorn x;
    utility::print_n(x, 4);
}
Run Code Online (Sandbox Code Playgroud)

你编译这个程序,运行它,然后......它崩溃了."什么?!没办法,"你说:"我刚刚打电话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++标准委员会接受.

  • 我想说这不是一个陷阱,而是一个功能:它允许您通过提供专门针对您的类型的实现来覆盖库行为.如果没有ADL,您将无法修改`print`的行为以适应您的`unicorn`类型.一个广泛使用的应用是`swap`:许多标准算法需要交换值; 你可以提供自己优化的`swpa`版本,它将通过ADL选择.当然,如果你可以在不需要时阻止这种覆盖会更好(就像你没有强制要求你的成员函数是虚拟的那样). (16认同)
  • @Chubsdad:这是ADL的一个巨大陷阱.问题是,您可以编写两个完全独立的库,并且在不知道您将遇到问题的情况下意外地遇到此问题.没有多少"细心"可以完全保护你免受这种伤害. (15认同)
  • 这是ADL的陷阱还是不仔细使用ADL的陷阱? (9认同)
  • @MSalters:问题是,混合库的显式语句并不总是如此明确.例如,考虑一下,如果你编写一个名为`merge`的命名空间作用域函数模板,它合并了两个东西,你传递了两个`std :: vector`对象.根据你是否包含`<algorithm>`(声明`std :: merge`),你会得到不同的结果. (9认同)
  • 问题是为什么程序员在调用`:: utility :: print()`时会写`print(x)`?如果我写`print(x)`,那么我*打算*调用ADL以便*找到*正确的重载(也可能在其他命名空间中).如果我不想要ADL,那么我会写`:: utility :: print(x)`.所以我不完全*同意这个答案.它主要是因为缺乏关于ADL的基本*知识.我同意@LucTouraille.:-) (9认同)
  • 不,两个独立的图书馆永远不会有这个问题.你_have_写一个混合它们的显式语句,如上例所示.即使在那里,你也会看到图书馆不喜欢混合使用:mystuff-print与mystuff-unicorn一起使用. (6认同)
  • 我认为这是一个避免"使用命名空间"的论据(好吧,除了你自己的命名空间),而不是针对ADL.如果指定命名空间,您的读者和编译器都会更好地理解您.对于print_n中的print调用,您可以指定命名空间或将其作为自定义点. (3认同)
  • 我认为`print_n()`的文档应该提到它多次调用`print()`(以便用户知道ADL问题),或者`print()`应该包含在`命名空间中detail`.在您给出的代码示例中,`print_n()`在`print()`上以ADL的形式有一个未记录的自定义点. (2认同)

Fra*_*kHB 6

接受的答案是完全错误的 - 这不是 ADL 的错误。它显示了在日常编码中使用函数调用的粗心反模式 - 忽视依赖名称并盲目依赖不合格的函数名称。

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

在接受答案的情况下,如果print_n确实需要 ADL print(即允许其被覆盖),则应使用不合格print作为明确通知进行记录,因此客户将收到一份应仔细声明的合同print,并且不当行为将由 承担全部责任my_stuff。否则就是一个bug print_n。修复方法很简单:print使用前缀进行限定utility::。这确实是 的一个错误print_n,但很难说是该语言中 ADL 规则的错误。

然而,语言规范中确实存在不需要的东西,而且从技术上讲,不只一个。它们的实现已有 10 多年的时间,但语言中的任何内容尚未得到解决。他们被接受的答案所错过(除了最后一段到目前为止是唯一正确的)。有关详细信息,请参阅本文

我可以附加一个针对令人讨厌的名称查找的真实案例。我正在实施is_nothrow_swappablewhere __cplusplus < 201703L. swap我发现一旦我的命名空间中有一个声明的函数模板,就不可能依赖 ADL 来实现这样的功能。在 ADL 规则下使用 ADL的习惯用法swap总是会发现这样的情况,然后调用模板(将实例化以获取正确的)的位置就会出现歧义。结合两阶段查找规则,一旦包含包含模板的库头,声明的顺序就不再计算在内。因此,除非我用专门的函数重载所有库类型(以抑制ADL 之后重载解析匹配的任何候选通用模板),否则我无法声明该模板。具有讽刺意味的是,在我的命名空间中声明的模板正是利用 ADL(考虑),并且它是我的库中最重要的直接客户端之一(顺便说一句,不尊重异常规范)。这完全违背了我的目的,叹息......std::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
}
Run Code Online (Sandbox Code Playgroud)

尝试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>;
Run Code Online (Sandbox Code Playgroud)

...所以它可以默认使用我的实现。polymorphic_allocator(免责声明:它有一些已知的错误。错误的修复将在几天内提交。)

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

错误从这一行开始。它粗略地抱怨声明的BaseType不是封闭类的基类MessageQueue这看起来很奇怪,因为别名是使用与类定义的基本说明符列表中的标记完全相同的标记来声明的,并且我确信它们都不能进行宏扩展。所以为什么?

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

我承认我足够幸运。错误消息将问题引向这一行。只有 2 个类似的问题,另一个没有任何明确的问题std我自己的问题string是适应 ISO C++17 的更改,而不是C++17 之前的模式中的问题)。我不会这么快就发现这个 bug 是关于 ADL 的。string_viewstd

  • 我认为现在大多数人都同意 ADL 规则的定义是一个错误(而不是 ADL 本身)。限定您自己的命名空间中符号的所有函数调用(在应用程序代码中是其中的大多数)是一件苦差事。这也会损害可读性。默认情况应该是相反的:明确标记哪些调用要执行 ADL。 (3认同)