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.

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • 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. – Ext3h Aug 02 '20 at 12:21
  • @Ext3h `at()` is great for debugging – Aykhan Hagverdili Aug 02 '20 at 12:21
  • @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. – Ext3h Aug 02 '20 at 14:20

2 Answers2

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;
}
Alberto Sinigaglia
  • 12,097
  • 2
  • 20
  • 48
  • Why are you using rvalue-reference parameters? doing move semantics? – JHBonarius Aug 02 '20 at 12:34
  • 1
    @Ayxan the `Container&&` should be a forward reference, see [here](https://stackoverflow.com/questions/3582001/what-are-the-main-purposes-of-using-stdforward-and-which-problems-it-solves/3582313#3582313), so you don't need to redefine it for each const/non const ecc ecc type, infact `const std::vector v{1,2,3}; std::cout << at(v, v.end());` should works fine – Alberto Sinigaglia Aug 02 '20 at 12:39
  • @JHBonarius see previous commet – Alberto Sinigaglia Aug 02 '20 at 12:41
  • 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. – Evg Aug 02 '20 at 12:50
  • @Evg so you are saying that might be better using const reference? – Alberto Sinigaglia Aug 02 '20 at 12:51
  • @Evg `std::vector::operator[]` does the same. No? – Aykhan Hagverdili Aug 02 '20 at 12:52
  • @Evg i can actually do something like this `std::cout << std::vector{"a","b","c"}[2];` or `std::vector{"a","b","c"}.at(2)`, so I think that is not that big of a deal – Alberto Sinigaglia Aug 02 '20 at 12:54
  • 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`](https://en.cppreference.com/w/cpp/utility/tuple/get): it returns `T&&` if given an rvalue tuple, or `delete` an overload that takes rvalues, like [`std::ref`](https://en.cppreference.com/w/cpp/utility/functional/ref). – Evg Aug 02 '20 at 12:58
  • 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. – Evg Aug 02 '20 at 13:02
  • @Evg because if you pass `c.end()+10`, with the `==` you won't get the real bound checking – Alberto Sinigaglia Aug 02 '20 at 13:03
  • 1
    `c.end() + 10` is [always undefined behaviour](https://stackoverflow.com/a/62711210), even for `std::vector`, and what happens after UB is irrelevant. – Evg Aug 02 '20 at 13:04
  • @Evg 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 used – Alberto Sinigaglia Aug 02 '20 at 13:07
  • @Evg also `span` contains contagious memory container, which will have a constant distance evaluation, since you can probably just use the `operator-` between the 2 iterators – Alberto Sinigaglia Aug 02 '20 at 13:08
  • 1
    @Ayxan, it does. But the free function [`std::get()`](https://en.cppreference.com/w/cpp/utility/tuple/get) does something different. Shouldn't free `at()` do the similar thing? I'm just asking. – Evg Aug 02 '20 at 13:18
  • 1
    `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. – Evg Aug 02 '20 at 13:21
  • @Evg 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. – Aykhan Hagverdili Aug 02 '20 at 13:29
  • 1
    @Ayxan Btw, GCC with `-D_GLIBCXX_DEBUG` reports assertion violation for out-of-bounds `[]` access on `std::span`: `Assertion '__idx < size()' failed.`. – Evg Aug 02 '20 at 13:40
  • Your function returns auto& but operator[] may return by value. I think you need to use something like decltype(auto). – Chris_F Nov 13 '22 at 13:07
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 implementor.

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

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • "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? – Aykhan Hagverdili Aug 02 '20 at 12:26
  • @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. – ecatmur Aug 02 '20 at 12:29
  • 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. – Aykhan Hagverdili Aug 02 '20 at 12:35
  • @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. – ecatmur Aug 02 '20 at 12:42
  • @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. – Yakk - Adam Nevraumont Aug 02 '20 at 12:54
  • @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`. – Aykhan Hagverdili Aug 02 '20 at 13:31
  • @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 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1743r0.pdf – ecatmur Aug 02 '20 at 16:59
  • @ecatmur I meant to say back in ye old days, we had `at` for our containers like vector and basic_string – Aykhan Hagverdili Aug 02 '20 at 17:09
  • @ecatmur 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. – Aykhan Hagverdili Aug 02 '20 at 17:21
  • @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. – Yongwei Wu Mar 08 '21 at 02:14