How do you do bounds checking with std span?
Asked Answered
S

3

6

std::vector and pretty much all other containers have a very convenient way of bounds checking: at(). std::span doesn't have that apparently.

Shizue answered 2/8, 2020 at 12:6 Comment(3)
Why would you want that in a release build? Bounds checking is an anti-pattern, and only makes sense in the context of input validation. Otherwise, it's complicating control flow under the hood, which prevents essential optimizations. You should be using the iterator interfaces, primarily. IMHO, at() and alike exception based interfaces were a misconception which serves primarily to conceal logic errors further up at a significant overhead.Lolita
@Lolita at() is great for debuggingShizue
@Axyan for debugging you have debug iterators (toggled by corresponding defines) in most implementations. But as I said, usually you would not want to see them in production code ever, either.Lolita
S
2

std::span::at will be added in C++26.

Shizue answered 26/11, 2023 at 21:36 Comment(0)
B
2

Pretty clunky but something like this:

  1. using position
template<class Container>
auto& at(Container&& c, std::size_t pos){
    if(pos >= c.size())
        throw std::out_of_range("out of bounds");
    return c[pos];
}
  1. using iterators:
template<class Iterator, class Container>
auto& at(Container&& c, Iterator&& it){
    if(std::distance(c.begin(), it) >= c.size())
        throw std::out_of_range("out of bounds");
    return *it;
}
Born answered 2/8, 2020 at 12:12 Comment(18)
Why are you using rvalue-reference parameters? doing move semantics?Medicinal
@Ayxan the Container&& should be a forward reference, see here, so you don't need to redefine it for each const/non const ecc ecc type, infact const std::vector<int> v{1,2,3}; std::cout << at(v, v.end()); should works fineBorn
@Medicinal see previous commetBorn
I would say that these signatures are dangerous. Forwarding references can bind to rvalues, and then you return a reference (auto&) to something that is going to perish in a moment.Lictor
@Lictor so you are saying that might be better using const reference?Born
@Lictor std::vector::operator[] does the same. No?Shizue
@Lictor i can actually do something like this std::cout << std::vector<std::string>{"a","b","c"}[2]; or std::vector<std::string>{"a","b","c"}.at(2), so I think that is not that big of a dealBorn
I just expressed my concern. Returning a reference to rvalue can be useful but is error-prone. What if you return && for rvalues? Like std::get: it returns T&& if given an rvalue tuple, or delete an overload that takes rvalues, like std::ref.Lictor
And why is it std::distance(c.begin(), it) >= c.size() and not it == c.end()? std::distance can take linear time, whereas iterator dereferencing is constant time.Lictor
@Lictor because if you pass c.end()+10, with the == you won't get the real bound checkingBorn
c.end() + 10 is always undefined behaviour, even for std::vector, and what happens after UB is irrelevant.Lictor
@Lictor Ayxan said that will use this code for debug, and so why we should care about this UB case when we can easily avoid it? PS: at() will get a size_t, so probably the version with the iteration won't be usedBorn
@Lictor also span contains contagious memory container, which will have a constant distance evaluation, since you can probably just use the operator- between the 2 iteratorsBorn
@Ayxan, it does. But the free function std::get() does something different. Shouldn't free at() do the similar thing? I'm just asking.Lictor
operator- looks like a good alternative. Then this (generic) code won't compile for iterators that don't support constant time std::distance, and this is good.Lictor
@Lictor I agree with both your points about rval reference and operator-. However, since this is only meant for debugging, it shouldn't matter either way.Shizue
@Ayxan Btw, GCC with -D_GLIBCXX_DEBUG reports assertion violation for out-of-bounds [] access on std::span: Assertion '__idx < size()' failed..Lictor
Your function returns auto& but operator[] may return by value. I think you need to use something like decltype(auto).Phytopathology
N
2

The paper that introduced span to the standard library says:

Range-checking and bounds-safety

All accesses to the data encapsulated by a span are conceptually range-checked to ensure they remain within the bounds of the span. What actually happens as the result of a failure to meet span’s boundssafety constraints at runtime is undefined behavior.

That is, the operations have narrow contracts, giving freedom to the implementer.

If your standard library doesn't let you control the behavior to the appropriate granularity. gsl-lite offers drop-in replacement with configurable contract violation behavior. The Microsoft GSL was previously configurable, but now always terminates on contract violation, discussed here (which might actually be what you want).

Nucleoprotein answered 2/8, 2020 at 12:19 Comment(10)
"If your standard library doesn't let you control the behavior to the appropriate granularity" What does that mean? I want to write portable code if possible. Is this a part of the standard interface or not?Shizue
@Ayxan portable as in you'll be using it with multiple implementations, or portable as in open source? At present an arbitrary implementation could well omit any checks, although that might change in future if Contracts ever happens.Nucleoprotein
Portable as in, if I use std::vector::at, I know it will be doing bounds checking whether I am using GCC, Clang, MSVC or anything.Shizue
@Ayxan you can check that by reading documentation, source code and/or testing. There's no guarantee in the standard - the behavior on bounds violation is specified to be undefined.Nucleoprotein
@ayxan an at that has defined behaviour on all inputs is known as a wide contract. One that does not is known as a narrow contract. This answer states that the interface being a narrow contract was on purpose. Narrow contracts permit bounds checking, but do not mandate it.Gulick
@Yakk-AdamNevraumont I think that was always the case for operator[], and alternatively we had a separate wider-contract function (at) and I was wondering why this was not added to span.Shizue
@ayxan I'm not aware that a wide-contract accessor was considered at any point. Wide contracts are generally depreciated for the Standard library; see open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1743r0.pdfNucleoprotein
@Nucleoprotein I meant to say back in ye old days, we had at for our containers like vector and basic_stringShizue
@Nucleoprotein while the paper explains why such wide contract is bad for production code, I wanted to use it for debugging purposes. I appreciate the link though, thank you.Shizue
@AyxanHaqverdili at throws an exception when the index is out of boundary. Going out of boundary in a span is regarded as violation of contract, and the behaviour is not controlled in code—as you can configure for no action, exception throwing, or termination.Luck
S
2

std::span::at will be added in C++26.

Shizue answered 26/11, 2023 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.