27
void foo(const auto& collection)
{
    *collection.begin() = 104;
}

int main()
{
    std::vector<int> ints {1, 2, 3, 4, 5};
    foo(ints); // Error, as it should be
    foo(ints | std::views::all); // Compiles and modifies the vector. Why?
    return 0;
}

Why is constness of lvalue reference completely ignored if an argument of a function is of type std::view?

Edit:
If, as you wrote in the comments, const view reference is similar to const pointer in this context, why does the code not compile if the same function takes a view constructed from an rvalue object as an argument?

std::vector<int> getVec()
{
    return std::vector{1, 2, 3, 4, 5};
}

void foo(const auto& collection)
{
    *collection.begin() = 104; // Error: assignment of read-only location
}

int main()
{
    foo(getVec() | std::views::all); // Nope!
    return 0;
}
Alexey104
  • 969
  • 1
  • 5
  • 17
  • 6
    I assume this is no different than `const std::span` vs `std::span`. – Chris_F Dec 15 '22 at 02:08
  • 4
    It's for the same reason why if you have an `int *p;` a `const` reference to it still allows you to modify `*p`. – Sam Varshavchik Dec 15 '22 at 02:09
  • 1
    `const` is shallow; it's not transitive. (I wish there was a transitive `deep_const`.) – Eljay Dec 15 '22 at 03:11
  • @Eljay related: there is the experimental "[`propagate_const`](https://en.cppreference.com/w/cpp/experimental/propagate_const)" for pointers and pointer-like objects. – starball Dec 16 '22 at 18:11
  • Standard views are [totally broken](https://www.youtube.com/watch?v=qv29fo9sUjY). – Evg Jun 28 '23 at 12:54

1 Answers1

28

Views, despite the name, need not be non-modifiable. In general, how a class that represents a sequence of contained objects propagates const depends on what class gets used. And that's where things get weird.

See, the return type of views::all(e) changes depending on exactly what e is. If e is a glvalue (and is not itself a view), then it returns a ref_view of the range denoted by e. ref_view behaves as if it stores a pointer to the range it is given. Of course, if you have an R* member of a class, the const equivalent of that is R * const, not R const*. So ref_view cannot propagate const to the contained range.

However, if e is a prvalue (and again is not a view), then what all returns is an owning_view. This object actually stores a copy of e (well, it moves from it) as a member. This means that when you get a const owning_view, that const is propagated to that member. And since const vector<T> does propagate const, so too will a views::all(e) of a prvalue of vector.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 4
    I was going to hold my tongue but ... "`owning_view`" was not part of the original design. Previously, you couldn't use the adaptors on a temporary container. The existence of "`owning_view`" muddies the design and contributes to this confusion. I'm sad it was ever added. – Eric Niebler Dec 16 '22 at 16:15
  • 2
    The design got muddled as soon as we got `views::single`. `owning_view` is simply a safer and far more efficient way to spell `views::single | views::join`. – T.C. Dec 17 '22 at 00:23