Home > Enterprise >  Bug or compilation error with some compilers for simple std::ranges code
Bug or compilation error with some compilers for simple std::ranges code

Time:11-02

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

Output:

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>')

Expected

a
bb
cc

Works as expected with GCC 11.2, 11.3, 12.1, 12.2, MSVC 19.33

CodePudding user response:

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:

How the argument is stored

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.

Brace vs. paren initialization

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.

CodePudding user response:

You're issue isn't even with the availability of ranges but rather due to the fact that your code does not use views appropriately, particularly you've misused stdv::transform (where stdv = std::views).

The function you pass to stdv::transform takes an invokable that is invoked on every element in the piped range. In this case, this is the inner std::vector of myvec. Because you have invoked ith_element you are not even invoking that function on each inner vector but rather, trying to invoke the view it returns; ie. stdv::drop(i) | stdv::take(1), which is ill formed as a view should not be invoked directly but rather have a range applied to it using the pipe notation.

Based on what you appear to want to do and given you're input of nested vectors, what you want is to transform each vector such that it only contains the ith element and join the resulting subranges into a single range. To do this requires a little reworking of your existing example and some help from std::bind.

#include <iostream>
#include <functional>
#include <ranges>
#include <string>
#include <vector>

namespace stdv = std::views;
using namespace std::placeholders;

/// Drops `i` elements of `v` and takes 1 
/// from the rest. Returns the resulting
/// view. Note: applies the view to the
/// range `v`.
auto ith = [](auto& v, std::size_t i)
{
    return v | stdv::drop(i)
             | stdv::take(1);
};

/// Takes the desired column index
/// and partially applies it to `ith`.
/// This partial `ith` is mapped over
/// the range which are joined.
auto column = [](std::size_t i)
{ return stdv::transform(std::bind(ith, _1, i)) | stdv::join; };

int main()
{
    std::vector<std::vector<std::string>> vec = {
        { "a", "aaa",   "aa"},
        {"bb",   "b", "bbbb"},
        {"cc",  "cc",  "ccc"}
    };

    for (const auto& v : vec | column(2))   
        std::cout << v << std::endl;

    return 0;
}

Output:

aa
bbbb
ccc

Godbolt Demo

On another note, what was mentioned in the comments by @Ted Lyngmo about the issues with the standard implementation of ranges being incomplete is true. This could very much be another issue with your current setup as apposed to the written code.

C 20 Compiler Support Page (ctrl f "The One Ranges Proposal" to see current support).

  • Related