【问题标题】:What are the pitfalls of ADL?ADL的陷阱是什么?
【发布时间】:2011-02-26 20:51:53
【问题描述】:

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

【问题讨论】:

标签: c++ namespaces overload-resolution argument-dependent-lookup


【解决方案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() 并将任何对象传递给它,它会调用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++ 标准委员会接受。

【讨论】:

  • 这是ADL的陷阱还是不小心使用ADL的陷阱?
  • @Chubsdad:这是 ADL 的一个巨大陷阱。问题是您可以编写两个完全独立的库,并且在不知道您会遇到问题的情况下意外遇到此问题。再多的“小心”也无法完全保护您免受这种伤害。
  • @MSalters:嗯,问题在于混合库的显式声明并不总是那么明确。考虑一下,例如,如果您编写一个名为 merge 的命名空间范围函数模板,它合并了两个东西,然后您将两个传递给两个 std::vector 对象。根据您是否包含&lt;algorithm&gt;(声明std::merge),您会得到不同的结果。
  • 我会说这不是一个陷阱,而是一个特性:它允许您通过提供专门针对您的类型的实现来覆盖库行为。如果没有 ADL,您将无法修改 print 的行为以适应您的 unicorn 类型。一个广泛使用的应用是swap:许多标准算法需要交换值;您可以提供自己的优化版本swpa,它会被 ADL 选中。当然,如果你能在不需要时阻止这种覆盖会更好(就像你没有被要求让你的成员函数成为虚拟一样)。
  • 问题是为什么程序员要调用::utility::print()时要写print(x)?如果我写print(x),那么我打算 调用ADL 以便找到 正确的重载(也可能在其他名称空间中)。如果我不想要 ADL,那么我会写 ::utility::print(x)。所以我不完全同意这个答案。这主要是因为缺乏关于 ADL 的基本知识。我会同意@LucTouraille。 :-)
【解决方案2】:

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

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

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

但是,确实在语言规范中存在不需要的东西,而且从技术上讲,只有一个。它们实现了 10 多年,但语言中的任何内容都没有固定下来。接受的答案错过了它们(除了最后一段直到现在才完全正确)。有关详细信息,请参阅此paper

我可以针对讨厌的名称查找附加一个真实案例。我正在实施is_nothrow_swappable,其中__cplusplus &lt; 201703L。一旦我的命名空间中有一个声明的 swap 函数模板,我发现不可能依靠 ADL 来实现这样的功能。这样的swap 总是会与由惯用的using std::swap; 引入的std::swap 一起找到,以在ADL 规则下使用ADL,然后会出现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++17 std::polymorphic_allocator 移植到我的代码库的工作。由于我的代码中很久以前就引入了一些容器类模板(例如this),所以这次我只是将声明替换为别名模板,例如:

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>;

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

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

错误从this line 开始。它大致抱怨声明的BaseType 不是封闭类MessageQueue 的基础。这看起来很奇怪,因为别名是用与类定义的 base-specifier-list 中完全相同的标记声明的,而且我确信它们中的任何一个都不能被宏扩展。那为什么呢?

答案是……ADL 很烂。 The line inroducing BaseType 是使用 std 名称作为模板参数硬编码的,因此将根据 ADL 规则在类范围内查找模板。因此,它找到了std::multimap,这与在封闭命名空间范围中声明的实际基类中查找的结果不同。由于std::multimap 使用std::allocator 实例作为默认模板参数,BaseType 与具有polymorphic_allocator 实例的实际基类的类型不同,即使在封闭命名空间中声明的multimap 也被重定向到@ 987654375@。通过在= 中添加封闭条件作为前缀权限,修复了该错误。

我承认我很幸运。错误消息将问题指向这一行。只有 2 个类似的问题,the other 没有任何明确的 std(其中 stringmy own one 适应 ISO C++17 的 string_view 更改,而不是 std 一个在 pre-C++ 17 种模式)。我不会这么快就发现这个错误是关于 ADL 的。

【讨论】:

  • 我认为现在大多数人都同意,定义的 ADL 规则是一个错误(而不是 ADL 本身)。在您自己的命名空间(在应用程序代码中是其中的大部分)中,对符号的所有函数调用进行限定是一件苦差事。它也损害了可读性。默认值应该是相反的:明确标记哪些调用是为了执行 ADL。
  • @Acorn 这在POLA 的意义上可能更好,但如果这是真的,我怀疑会有专门为 ADL 设计的区别语法。不过,可能还有其他选择。无论如何,ADL 是翻译过程中“通常”名称查找规则的替代者,那么为什么不使用一些更通用的元编程工具来使其可编程(例如hygienic macros)?但遗憾的是,设计中没有考虑到这一点。
猜你喜欢
  • 2011-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多