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

1

5

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?

Mideast answered 21/5, 2023 at 10:29 Comment(10)
Related Overload of std::ostream for __int128_t ambiguous. Also refer to how to ask where the first step is to search and then research and you'll find related SO posts.Charentemaritime
See also dupe1 Output 128 bit integer using stream operatorCharentemaritime
@Jason The duplicate is not a good one, because the exact type of idx is an implementation detail of rv::iota that shouldn't be relevant to whether or not OP's code is valid.Furfuraceous
@Furfuraceous Dupe2 How to print __int128 in g++?Charentemaritime
@Furfuraceous There was no need to reopen the post as there are also many other duplicates that can be added to the list. For example cout cannot print __int128 - MinGW-w64 is dupe3.Charentemaritime
@Jason The question here is about whether or when it is guaranteed that the result of rv::iota(0zu, 5zu) | rv::enumerate can be printed with << and if it isn't how it can be printed. __in128 appears nowhere in this and is an implementation-detail. The user should not need to know or rely on the exact type.Furfuraceous
@Furfuraceous The duplicates can be used to solve the error. If yes, then the targets are applicable as duplicates.Charentemaritime
@Jason No, with the solutions given in these answers, OP's code would still have unspecified behavior because the actual type of idx is unspecified.Furfuraceous
@Furfuraceous I see.Charentemaritime
The iota_view::iterator is supposed to choose a difference_type that is wider than the type iterated over. IOTA-DIFF-T(W) So not really a compiler bug.Sheff
F
7

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.

Furfuraceous answered 21/5, 2023 at 11:14 Comment(1)
Ok I see, it actually makes a lot of sense. I will have to think about how to handle it properly. My example is a heavy simplification of the situation. In my case the range depends on some Matrix sizes (hence the use of std::size_t) and it happens that the matrix accessors are not overloaded for this __int128 type. But knowing where the problem lies should help, thanks :)Mideast

© 2022 - 2024 — McMap. All rights reserved.