2

Consider the following useless code:

#include <ranges>
#include <source_location>
#include <iostream>

int main() {
  auto lines = std::views::iota(0, 5)
             | std::views::transform(
                [](int, const std::source_location& location = std::source_location::current()) 
                { return location.line(); }
               );
  for (const auto& line : lines)
    std::cout << line << "\n";
}

MSVC rejects with the strange error message:

(7): error C2676: binary '|': 'std::ranges::iota_view<_Ty1,_Ty2>' does not define this operator or a conversion to a type acceptable to the predefined operator
            with
            [
                _Ty1=int,
                _Ty2=int
            ]

And GCC outputs strange line number 61 no matter which row the std::source_location::current() is in:

61
61
61
61
61

Is the above code well-formed? If so, does it mean that both MSVC and GCC have bugs?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • 3
    Can't tell you about MSVC, but I'm 99.9% sure default arguments are evaluated at the time of the function call, and for a closure object, that will be inside the code for the range function you are using, which would make line 61 kind of make sense. Use `file_name` to confirm. – NathanOliver Aug 17 '21 at 16:43
  • 1
    "*And GCC outputs strange line number 61 no matter which row the std::source_location::current() is in:*" Why is this surprising? Won't the function be called at the same place within the loop? – Nicol Bolas Aug 17 '21 at 16:44
  • 1
    @NicolBolas. I mean, why not line `8`? How did this `61` come from? – 康桓瑋 Aug 17 '21 at 16:45

1 Answers1

3

gcc is correct, the program is completely valid.

And GCC outputs strange line number 61 no matter which row the std::source_location::current() is in:

That's because the default function argument, current(), is evaluated at the point of the function call, which has nothing to do with where the function is declared.

And this function is called by transform_view's iterator's operator*(). But not directly by operator*(), that operator is going to call invoke which itself is going to have to do a bunch of work to make sure it's invoked correctly. And the actual final overload in libstdc++'s implementation of invoke that gets called is... oh, look at that, it's bits/invoke.h:61:

template<typename _Res, typename _Fn, typename... _Args>
  constexpr _Res
  __invoke_impl(__invoke_other, _Fn&& __f, _Args&&... __args)
  { return std::forward<_Fn>(__f)(std::forward<_Args>(__args)...); }

This would've been easier to discover if instead of just printing the line number that source_location gives you, you also printed the file name:

auto lines = std::views::iota(0, 5)
           | std::views::transform(
              [](int, const std::source_location& location = std::source_location::current()) 
              { return fmt::format("{}:{}", location.file_name(), location.line()); }
             );

fmt::print("{}\n", lines);

Which prints a range containing the string /opt/compiler-explorer/gcc-trunk-20210817/include/c++/12.0.0/bits/invoke.h:61, five times.

Barry
  • 286,269
  • 29
  • 621
  • 977