Search code examples
c++implicit-conversionstd-rangesc++23

Why is the compiler unable to match the types automatically on `size_t` variables in a ranged base for loop?


I stumbled upon a problem with ambiguous overload for operator<< when using std::views::enumerate with size_t range. More specifically using this code:

#include <iostream>
#include <ranges>
namespace rv = std::ranges::views;

int main()
{
    for (const auto& [idx, value] : rv::iota(0zu, 5zu) | rv::enumerate)
        std::cout << idx << '\n';
}

and compiling with gcc 13.1.1 on linux using: g++ --std=c++23 main.cpp. I get the error:

main.cpp: In function ‘int main()’:
main.cpp:11:19: error: ambiguous overload for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘std::tuple_element<0, const std::tuple<__int128, long unsigned int> >::type’ {aka ‘const __int128’})
   11 |         std::cout << idx << '\n';
      |         ~~~~~~~~~ ^~ ~
      |              |       |
      |              |       std::tuple_element<0, const std::tuple<__int128, long unsigned int> >::type {aka const __int128}
      |              std::ostream {aka std::basic_ostream<char>}

and then a bunch of candidates for the << operator.

This can be remediated by casting idx (e.g., using std::cout << (size_t)idx) but it seems unnecessary tedious. The problem seems to only occur when using long variables as start and end for the range. For instance if we use rv::iota(begin, end) with either int begin{0}, end{5} (whether signed or unsigned) the problem disappears.

Is this a simple bug at the compiler level or is there something deeper preventing it from matching the correct type?


Solution

  • The type of idx is the difference_type of the iota view.

    You are using the type std::size_t for the arguments to iota. Probably std::size_t has 64 bit width on your system and that is probably also the maximum width of any integer type on your system.

    The problem now is that difference_type for the iota view cannot also be a 64 bit width type, since such a type wouldn't be able hold the difference between any two items in the range.

    Therefore iota is specified to have a difference_type that has larger width than the width of the element type. If a signed integer type with that property exists, then difference_type will be a signed integer type. If such an integer doesn't exits, then difference_type will be a signed-integer-like type with sufficient width, i.e. a type that behaves in certain important ways like a signed integer type, but isn't one.

    It could be e.g. a class type with properly overloaded operators or, as you are seeing, some implementation-specific type that is not considered an integer type, but has similar behavior. (Actually, whether GCC considers __int128 an extended integer type or not depends on whether you use -std=gnu++23 or -std=c++23.)

    For a list of properties these types need to satisfy see [iterator.concept.winc].

    However, std::ostream::operator<< is overloaded only for the standard integer types. idx's type therefore doesn't have an exactly matching overload if it is only a signed-integer-like type.

    You get the ambiguity error because the concrete type the implementation chose for idx, i.e. __in128_t, is implicitly convertible to all of the standard signed integer types with same conversion rank. (That would also be the case if __int128_t was considered an (extended) signed integer type.) However, this is an implementation detail. It isn't even guaranteed that the signed-intger-like type is implicitly convertible to any of the integer types (because they all have smaller width).

    The explicit conversion with e.g. static_cast or a C-style cast is guaranteed to work to any integer type, but risks narrowing the result.

    So there is no compiler bug here. This is the allowed behavior and you can't rely on being able to print idx with operator<< directly.

    If you intent iota to only consider a range of 0 to 5, the easiest solution would be to not use type std::size_t, but a smaller type of sufficient size instead, e.g. rv::iota(0, 5) if you know that int is 32 bit and you are on a 64 bit system.

    If you do not intent to risk narrowing and you don't want to rely on knowing that there is a standard integer type with larger width on your system, then you'll have to convert the value of idx to a decimal string representation yourself. The usual arithmetic operators are guaranteed to work on the signed-integer-like type as expected, so that this is possible. But I don't think there is currently any standard library function that converts any signed-integer-like type to a decimal string representation.