4

I work on a C++ project that uses a lot of variables of type __int128_t and __uint128_t (herafter int128 and uint128 for bevity). In order to make "cout debugging" easier, we wrote a std::ostream overload for int128 and uint128 types, similar to what is shown in this answer (https://stackoverflow.com/a/25115163/753174) except it handles std::hex, std::setw, std::setfill, etc.

It works fine when called from within the same namespace as the ostream overload (e.g. if both are in the global namespace). It also works fine if called within a namespace that does not have any other ostream overloads in it. For example, this compiles fine and works:

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

But if the ostream operator is used within a namespace that has any other std::ostream overload (even something completely unrelated like, any struct, class, enum), I get a compile error like this (tried gcc 8.3 and 10.2, and clang 9.0.1)

Example that causes the error (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;
}

Full compiler output of error:

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

I can get around this by redeclaring my int128 ostream overload in every namespace where I want to use it, or by calling a to_string() function explicitly instead of relying on ostream overloads. These workarounds are not ideal though. Am I missing something or is this just how it is? As far as I can tell, GCC and clang still lack ostream overloads for these 128-bit types, despite them being available for many years.

ScottG
  • 773
  • 6
  • 18
  • Added full error output from compiler, I had snipped it since I thought it was too verbose. – ScottG Feb 04 '21 at 20:32
  • Since `ABC` is in a different namespace, you won't get ADL convenience. You'll just need to fully namespace qualify everything relevant, or put ABC in the same `Something` namespace. – Eljay Feb 04 '21 at 20:41
  • I don't care about ABC, its just a dummy struct showing that as soon as there is an overload for *any* type within the Something namespace, I get this compiler error when trying to print int128/uint128. – ScottG Feb 04 '21 at 21:11

2 Answers2

3

With using you make the name ::operator<< part of the namespace as the one which is already present for ABC. Then the compiler can choose the most appropriate at call site.

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

Without this artificial injection of the name ::operator<<, when << exists for another type (ABC) in this namespace, there is no need to look further (in global namespace). The argument-dependent-lookup (ADL) also injects the << name from std:: because of std::ostream. After that, the compiler chooses the best match between all of these possible << considered as accessible. The one for ABC does not match but the other ones (for integers, reals...) could be used with a conversion; but which one is the best? This is ambiguous.

On the contrary, when << for ABC does not exist, there is no such << name in the current namespace, then this name is looked for in the upper (global) namespace; here a perfect match exists for uint128, so the candidates from std:: (ADL) are not considered as potential better match.

This not easy to follow because there are two stages.

First, look for the << name. This starts from the current namespace; if found it stops here, if not found it goes on in the upper namespace and so on until the global namespace. But ADL takes also place and injects << names from other namespaces based on arguments at call site (std:: here for ostream).

Second, choose the best match between all these collected <<. If there is no perfect match, a conversion could be considered but if many conversions are possible, this is ambiguous.

Trying to illustrate the various situations:

• 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 *)!!!
prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • Adding `using ::operator<<` to my namespace works. Is there any downside of this approach? – ScottG Feb 04 '21 at 21:16
  • @ScottG no, I don't think there is a downside, and even more, I suppose this is the purpose of `using` (inject some names from elsewhere). – prog-fh Feb 04 '21 at 21:20
  • @ScottG I tried to *simulate* the process in the answer. – prog-fh Feb 04 '21 at 21:39
  • This was a great explanation. I had no idea that a non-matching overload in the current namespace causes the compiler to stop looking for matches in outer/global namespaces. – ScottG Feb 05 '21 at 14:27
3

Your problem is that operators should be defined in the namespace of one of the types involved so they can be found via ADL.

In this case, one of the types is in std, and adding overloads there is verboten, and the other built-in, and has no associated namespace.

Lookup for a function or operator to find candidates for overload resolution has 2 "forks". (This is an over simplification)

The first is normal lookup. Roughly (the rules are complex), this stops when it finds anything that matches the name.

The second is ADL, argument dependent lookup. This finds name matches in the namespaces of the associated arguments.

ADL can't work here, and the other is blocked by functions/operators with matching names and not argument types.


A few fixes:

  1. Do using ::mystream::operator<< wherever you need it. (Put it in a namespace; pull it to global namespace if you must)

  2. Write an ostream wrapper that allows for extension.

  3. Write a print wrapper that wraps the integer type.

Both 2 and 3 allow for ADL-based operator<< to be found. 3 is similar to to_string, but you can make it print directly.

For 2, you'd start with:

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

then write code that detects if OS&<<X is valid, and add << overloads to myOS that forward if so.

Now, notstd::cout can be a myOS wrapper.

Then add your 128 bit int overloads in notstd that stream to myOS<T>.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Other fix idea: Wrap the `__int128_t` in a real `class`, instead of a `using`, so you have a namespace associated with that class for ADL purposes. – MSalters Feb 05 '21 at 11:09
  • @msal there are ABI issues with structs; they get passed on stack amd never in registers. Dunno if that is a big hit here. – Yakk - Adam Nevraumont Feb 05 '21 at 12:30