【问题标题】:Overload of std::ostream for __int128_t ambiguous (when called from a namespace)__int128_t 模棱两可的 std::ostream 重载(从命名空间调用时)
【发布时间】:2021-05-09 04:34:21
【问题描述】:

我在一个 C++ 项目中工作,该项目使用了很多类型为 __int128_t__uint128_t 的变量(以下为 int128uint128 以表示敬意)。为了使“cout 调试”更容易,我们为 int128uint128 类型编写了一个 std::ostream 重载,类似于此答案 (https://stackoverflow.com/a/25115163/753174) 中显示的内容,只是它处理 std::hex、std: :setw、std::setfill 等

当从与 ostream 重载相同的命名空间中调用时,它可以正常工作(例如,如果两者都在全局命名空间中)。如果在其中没有任何其他 ostream 重载的命名空间中调用,它也可以正常工作。例如,这编译得很好并且可以工作:

#include <string>
#include <iostream>
#include <stdint.h>

using uint128 = __uint128_t;
using int128 = __int128_t;

inline std::ostream &operator<<(std::ostream &out, uint128 val)
{
    //Obviously not the real implementation, just here as an example
    return out << static_cast<uint64_t>(val);
}

void print_uint128(uint128 val)
{
    std::cout << "A uint128: " << val << std::endl;
}

namespace Something
{
    void print_uint128(uint128 val)
    {
        std::cout << "A uint128: " << val << std::endl;
    }
}

int main()
{
    uint128 foo = 1234;
    print_uint128(foo);
    Something::print_uint128(foo);
    return 0;
}

但是,如果在具有 any 其他 std::ostream 重载(甚至是完全不相关的东西,例如任何结构、类、枚举)的命名空间中使用 ostream 运算符,我会收到类似的编译错误这个(尝试过 gcc 8.3 和 10.2,以及 clang 9.0.1)

导致错误的示例 (https://godbolt.org/z/sozrx7):

#include <string>
#include <iostream>
#include <stdint.h>

using uint128 = __uint128_t;
using int128 = __int128_t;

//Doesn't matter what ABC is, an empty struct demonstrates the
//issue, but same happens if ABC is an enum or enum class
struct ABC { };

namespace Something {
std::ostream &operator<<(std::ostream &os, ABC def)
{
    //Doesn't matter what this does
    return os;
}
}

inline std::ostream &operator<<(std::ostream &out, uint128 val)
{
    return out << static_cast<uint64_t>(val);
}


void print_uint128(uint128 val)
{
    std::cout << "An int128: " << val << std::endl;
}

namespace Something
{
void print_uint128(uint128 val)
{
    std::cout << "An int128: " << val << std::endl;
}
}

int main()
{
    uint128 foo = 1234;
    print_uint128(foo);
    Something::print_uint128(foo);
    return 0;
}

错误的完整编译器输出:

<source>:36:32: error: use of overloaded operator '<<' is ambiguous (with operand types 'basic_ostream<char, std::char_traits<char> >' and 'uint128' (aka 'unsigned __int128'))
    std::cout << "An int128: " << val << std::endl;
    ~~~~~~~~~~~~~~~~~~~~~~~~~~ ^  ~~~
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:166:7: note: candidate function
      operator<<(long __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:170:7: note: candidate function
      operator<<(unsigned long __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:174:7: note: candidate function
      operator<<(bool __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:178:7: note: candidate function
      operator<<(short __n);
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:181:7: note: candidate function
      operator<<(unsigned short __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:189:7: note: candidate function
      operator<<(int __n);
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:192:7: note: candidate function
      operator<<(unsigned int __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:201:7: note: candidate function
      operator<<(long long __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:205:7: note: candidate function
      operator<<(unsigned long long __n)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:220:7: note: candidate function
      operator<<(double __f)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:224:7: note: candidate function
      operator<<(float __f)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:232:7: note: candidate function
      operator<<(long double __f)
      ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:517:5: note: candidate function [with _Traits = std::char_traits<char>]
    operator<<(basic_ostream<char, _Traits>& __out, char __c)
    ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:511:5: note: candidate function [with _CharT = char, _Traits = std::char_traits<char>]
    operator<<(basic_ostream<_CharT, _Traits>& __out, char __c)
    ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:523:5: note: candidate function [with _Traits = std::char_traits<char>]
    operator<<(basic_ostream<char, _Traits>& __out, signed char __c)
    ^
/opt/compiler-explorer/gcc-9.2.0/lib/gcc/x86_64-linux-gnu/9.2.0/../../../../include/c++/9.2.0/ostream:528:5: note: candidate function [with _Traits = std::char_traits<char>]
    operator<<(basic_ostream<char, _Traits>& __out, unsigned char __c)
    ^
1 error generated.

我可以通过在我想使用它的每个命名空间中重新声明我的 int128 ostream 重载,或者通过显式调用 to_string() 函数而不是依赖于 ostream 重载来解决这个问题。但是,这些解决方法并不理想。我错过了什么还是这就是它的样子?据我所知,GCC 和 clang 仍然缺乏这些 128 位类型的 ostream 重载,尽管它们已经存在多年。

【问题讨论】:

  • 添加了编译器的完整错误输出,因为我认为它太冗长,所以我剪掉了它。
  • 由于 ABC 位于不同的命名空间中,因此您不会获得 ADL 便利。您只需要完全命名空间限定所有相关内容,或将 ABC 放在同一个 Something 命名空间中。
  • 我不关心 ABC,它只是一个虚拟结构,表明只要在 Something 命名空间中存在 any 类型的重载,我就会收到此编译器错误尝试打印 int128/uint128 时。

标签: c++


【解决方案1】:

使用using,您将名称 ::operator&lt;&lt; 作为ABC 已经存在的名称空间的一部分。 然后编译器可以在调用点选择最合适的。

namespace Something
{
  using ::operator<<;
  void print_uint128(uint128 val)
  {
    std::cout << "An int128: " << val << std::endl;
  }
}

如果没有这个人工注入名称::operator&lt;&lt;,当&lt;&lt;在这个命名空间中存在另一种类型(ABC)时,不需要进一步查看(在全局命名空间中) . 由于 std::ostream,参数相关查找 (ADL) 还从 std:: 注入了 &lt;&lt; 名称。 之后,编译器在所有这些可能的&lt;&lt; 被认为是可访问的之间选择最佳匹配。 ABC 的一个不匹配,但其他的(对于整数、实数...)可以用于转换;但哪一个是最好的? 这是模棱两可的。

反之,当&lt;&lt; for ABC不存在时,当前命名空间中不存在&lt;&lt;这样的名字,则在上层(全局) 命名空间;这里uint128 存在完美匹配,因此来自std:: (ADL) 的候选人不被视为潜在的更好匹配。

这不容易理解,因为有两个阶段。

首先,查找&lt;&lt; 名称。 这从当前命名空间开始;如果找到它会停在这里,如果没有找到它会在上层命名空间中继续,依此类推,直到全局命名空间。但 ADL 也会发生并根据调用站点的参数从其他命名空间注入 &lt;&lt; 名称(std:: 此处为 ostream)。

其次,在所有收集到的&lt;&lt; 之间选择最佳匹配。 如果没有完美匹配,则可以考虑转换,但如果可以进行多次转换,则这是模棱两可的。

试图说明各种情况:

• NO << (for ABC)  in current namespace,
  << (for uint128)  in global namespace,
  1 --> no << in the current namespace then look in the upper namespace,
        find << (for uint128) in global namespace
        + many << from std:: via ADL
  2 --> the one from the global namespace is a perfect match --> OK!

• << (for ABC) in current namespace,
  << (for uint128) in global namespace,
  1 --> find << (for ABC) in _current_ namespace and _STOP_ here
        + many << from std:: via ADL
  2 --> no one is a perfect match,
        ABC does not match at all,
        various integers/reals could match with conversions --> AMBIGUOUS!

• << (for ABC) in current namespace,
  << (for uint128) in global namespace,
  using ::operator<< in current namespace
  1 --> find << (for ABC _AND_ for uint128) in _current_ namespace and stop here
        + many << from std:: via ADL
  2 --> ABC does not match at all,
        various integers/reals could match with conversions,
        the one for uint128 is the only perfect match --> OK!

• << (for ABC) in current namespace,
  << (for uint128 *) in global namespace,
  1 --> find << (for ABC) in _current_ namespace and _STOP_ here
        + many << from std:: via ADL
  2 --> ABC does not match at all,
        various integers/reals do not match at all
        std::operator<< for void * matches --> OK!!!!!!!!!!!!
        _A_MATCH_IS_FOUND_BUT_NOT_YOURS_ (void * not uint128 *)!!!

【讨论】:

  • using ::operator&lt;&lt; 添加到我的命名空间有效。这种方法有什么缺点吗?
  • @ScottG 不,我不认为有缺点,甚至更多,我想这就是using 的目的(注入一些来自其他地方的名字)。
  • @ScottG 我试图模拟答案中的过程。
  • 这是一个很好的解释。我不知道当前命名空间中的不匹配重载会导致编译器停止在外部/全局命名空间中查找匹配项。
【解决方案2】:

您的问题是运算符应该在所涉及类型之一的命名空间中定义,以便可以通过 ADL 找到它们。

在这种情况下,其中一种类型在std 中,并且在那里添加重载是禁止的,而另一种是内置的,并且没有关联的命名空间。

查找函数或运算符以查找重载解决方案的候选者有 2 个“分叉”。 (这是过于简单化了)

首先是正常查找。粗略地说(规则很复杂),当它找到与名称匹配的 anything 时,停止

第二个是 ADL,参数依赖查找。这会在关联参数的命名空间中找到名称匹配项。

ADL 在这里不起作用,另一个被名称匹配而不是参数类型的函数/运算符阻塞。


一些修复:

  1. using ::mystream::operator&lt;&lt; 随时随地发送。 (把它放在一个命名空间;如果你必须把它拉到全局命名空间)

  2. 编写一个允许扩展的ostream 包装器。

  3. 编写一个包装整数类型的print 包装器。

2 和 3 都允许找到基于 ADL 的 operator&lt;&lt;。 3和to_string类似,但是可以直接打印出来。

对于 2,您将从:

template<class OS>
struct myOS{
  OS& os;
};

然后编写检测OS&amp;&lt;&lt;X 是否有效的代码,如果有效,则将&lt;&lt; 重载添加到转发的myOS

现在,notstd::cout 可以成为 myOS 包装器。

然后将 notstd 中的 128 位 int 重载添加到 myOS&lt;T&gt;

【讨论】:

  • 其他修复思路:将__int128_t 包裹在真正的class 中,而不是using,这样您就有了一个与该类关联的命名空间以用于ADL。
  • @msal 结构存在 ABI 问题;它们在堆栈上传递,从不在寄存器中传递。不知道这是否在这里很受欢迎。
猜你喜欢
  • 1970-01-01
  • 2011-12-03
  • 2011-03-13
  • 1970-01-01
  • 2013-09-16
  • 1970-01-01
  • 2012-11-08
  • 2011-06-30
  • 1970-01-01
相关资源
最近更新 更多