I have a piece of code that uses the ranges library of C++20, taken from this SO anwer. The code is rejected by some compiler (versions) and some older GCC versions return garbage. Which compiler is right?
The code is supposed to print the elements of the first column in a std::vector<std::vector>.
#include <vector>
#include <string>
#include <ranges>
#include <iostream>
int main()
{
// returns a range containing only the i-th element of an iterable container
auto ith_element = [](size_t i) {
// drop the first i elements in the range and take the first element from the remaining range
return std::views::drop(i) | std::views::take(1);
};
// returns a range over the i-th column
auto column = [ith_element](size_t i) {
return std::views::transform(ith_element(i)) | std::views::join; // returns a range containing only the i-th elements of the elements in the input range
};
std::vector<std::vector<std::string>> myvec = {
{ "a", "aaa", "aa"},
{"bb", "b", "bbbb"},
{"cc", "cc", "ccc"}
};
for (auto const& v: myvec | column(0)){
std::cout << v << std::endl;
}
return 0;
}
Compiler Explorer
With GCC 10.1, 10.2, 10.3, 10.4:
b
Doesn't compile with GCC 11.1 and clang.
error: no match for 'operator|' (operand types are 'std::vector<std::vector<std::__cxx11::basic_string<char> > >' and 'std::ranges::views::__adaptor::_Pipe<std::ranges::views::__adaptor::_Partial<std::ranges::views::_Transform, std::ranges::views::__adaptor::_Pipe<std::ranges::views::__adaptor::_Partial<std::ranges::views::_Drop, long unsigned int>, std::ranges::views::__adaptor::_Partial<std::ranges::views::_Take, int> > >, std::ranges::views::_Join>')
a
bb
cc
Works as expected with GCC 11.2, 11.3, 12.1, 12.2, MSVC 19.33
GCC 11.2, 11.3, 12.1, 12.2, MSVC 19.33 are correct.
Clang up to 15 does not support libstdc++'s <ranges> at all, and with libc++ the program works correctly.
GCC 10.1, 10.2, 10.3, 10.4 and 11.1 mishandle std::views::drop(i) which is used in ith_element(i).
Here's why std::views::drop(i) is complicated, and how old GCC did it wrong:
To make range | std::views::drop(i) work, the result of std::views::drop(i) must memorize the value of i. But should it store a copy of i, or a reference to i?
GCC 10.x stores a reference when the argument is an lvalue. Hence, with GCC 10.x, the result returned by return std::views::drop(i) | std::views::take(1); contains a dangling reference.
GCC 11.1 implements P2281R1 Clarifying range adaptor objects which makes such objects always store a copy of the argument.
Does range | std::views::drop(i) initialize the result with braces (std::ranges::drop_view{range, i}) or with parentheses (std::ranges::drop_view(range, i))?
The relevant constructor of drop_view is drop_view(V, range_difference_t<V>). Note that the second parameter is a signed integer type, and list initialization disallows unsigned to signed conversion (because it's a narrowing conversion). Thus, with braces, the corresponding argument cannot be an unsigned integer (e.g. size_t).
GCC 10.x somehow permits this conversion, while GCC 11.1 (with P2281R1 implemented) rejects it. Hence, with GCC 11.1, uses of std::views::drop(i) is always an error when i is size_t.
GCC 11.2 and 12 implement P2367R0 Remove misuses of list-initialization from Clause 24, which changes std::views::drop to use parentheses instead of braces, and thus allows conversion from size_t to the difference type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With