为什么我的特征模板类查找运算符<< for llvm :: StringRef?

gig*_*tes 5 c++ llvm type-traits c++11 llvm-c++-api

下面的问题如何检测类型是否可以流式传输到std :: ostream?我写了一个trait类,说明某些类型是否可以流式传输到IO流.直到现在我发现了一个问题,这个特性似乎运作良好.

我在使用LLVM的项目中使用代码,我正在使用他们的StringRef类(它与提议的std :: string_view类似).是该类的Doxygen doc的链接,如果需要,您可以从中找到它的声明头文件.由于LLVM不提供运算符<<将StringRef对象流式传输到std流(它们使用自定义轻量级流类),因此我写了一个.

但是,当我使用特征时,如果我的自定义运算符<< 在特征之后被声明(这是因为我在一个标题中具有特征而操作符在另一个标题中的函数),则它不起作用.我曾经认为模板实例化中的查找是从实例化点的角度来看的,所以我认为它应该可行.实际上,正如你在下面看到的那样,使用另一个类及其自定义运算符<<,在特征之后声明,一切都按预期工作(这就是为什么我现在才发现这个问题),所以我无法弄清楚StringRef的原因特别.

这是完整的例子:

#include <iostream>

#include "llvm/ADT/StringRef.h"

// Trait class exactly from the cited question's accepted answer
template<typename T>
class is_streamable
{
   template<typename SS, typename TT>
   static auto test(int)
      -> decltype(std::declval<SS&>() << std::declval<TT>(),
                  std::true_type());

   template<typename, typename>
   static auto test(...) -> std::false_type;

public:
   static const bool value = decltype(test<std::ostream,T>(0))::value;
};

// Custom stream operator for StringRef, declared after the trait
inline std::ostream &operator<<(std::ostream &s, llvm::StringRef const&str) {
   return s << str.str();
}

// Another example class
class Foo { };
// Same stream operator declared after the trait
inline std::ostream &operator<<(std::ostream &s, Foo const&) {
    return s << "LoL\n";
}

int main()
{
   std::cout << std::boolalpha << is_streamable<llvm::StringRef>::value << "\n";
   std::cout << std::boolalpha << is_streamable<Foo>::value << "\n";

   return 0;
}
Run Code Online (Sandbox Code Playgroud)

与我的期望相反,这打印:

false
true
Run Code Online (Sandbox Code Playgroud)

如果我在特征声明之前移动运算符<< for StringRef 的声明,则它会输出true.那么为什么会发生这种奇怪的事情呢?我该如何解决这个问题呢?

Mat*_* M. 1

正如 Yakk 所提到的,这就是 ADL:参数依赖查找。

\n\n

如果您不想打扰,请记住,您应该始终在与其至少一个参数相同的命名空间中编写一个自由函数。在您的情况下,由于禁止将函数添加到std,这意味着将您的函数添加到llvm命名空间中。事实上,你需要用以下条件来限定StringRef论证,这一事实llvm::是一个彻底的泄露。

\n\n

函数解析的规则相当复杂,但可以简单概括一下:

\n\n
    \n
  • 姓名查找:收集一组潜在候选人
  • \n
  • 重载决策:在潜力中选择最佳候选者
  • \n
  • 专业化解决方案:如果候选者是函数模板,请检查是否有任何可以应用的专业化
  • \n
\n\n

我们这里关心的名称查找阶段相对简单。简而言之:

\n\n
    \n
  • 它扫描参数的名称空间,然后扫描它们的父名称空间,...直到到达全局范围
  • \n
  • 然后继续扫描当前作用域,然后是其父作用域,...直到到达全局作用域
  • \n
\n\n

可能是为了允许隐藏(就像任何其他名称查找一样),查找会在遇到匹配项的第一个范围处停止,并傲慢地忽略任何周围的范围。

\n\n

请注意,using指令(using ::operator<<;例如)可用于引入另一个范围的名称。但它很麻烦,因为它把责任放在了客户身上,所以请不要依赖它的可用性作为马虎的借口(我已经看到这样做了:x)。

\n\n
\n\n

阴影示例:此打印"Hello, World"不会引发歧义错误。

\n\n
#include <iostream>\n\nnamespace hello { namespace world { struct A{}; } }\n\nnamespace hello { void print(world::A) { std::cout << "Hello\\n"; } }\n\nnamespace hello { namespace world { void print(A) { std::cout << "Hello, World\\n"; } } }\n\nint main() {\n    hello::world::A a;\n    print(a);\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

中断搜索的示例:::hello::world产生一个名为printso 的函数,即使它根本不匹配,并且::hello::print严格来说是更好的匹配,但它还是被挑选出来。

\n\n
#include <iostream>\n\nnamespace hello { namespace world { struct A {}; } }\n\nnamespace hello { void print(world::A) { } }\n\nnamespace hello { namespace world { void print() {} } };\n\nint main() {\n    hello::world::A a;\n    print(a); // error: too many arguments to function \xe2\x80\x98void hello::world::print()\xe2\x80\x99\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n