3

One of the complaints about C++ is the lack of automated bounds checking, which lead to memory errors.

But the C++ STL containers provide a quite convenient .at() method for bounds checking a container access. Sure, it comes with a small performance impact, as opposed to the more popular operator[], but usually this is not an issue.

What are the pitfalls of using a code standard that mandates the usage of .at() for container access?

3
  • 4
    Some nit-picking: the focus of this question is on std::vector and possibly std::array, which allows three levels of "checkedness": .at(k) (always checked), operator[](k) (decided by compiler switches and preprocessor defines), and .data()[k] (never checked). The at() method has special meaning (behavioral difference) in std::map and std::unordered_map : at() does not auto-insert a non-existent key; operator [] always auto-inserts. A const map can only be accessed with at().
    – rwong
    Jun 7 at 14:09
  • 1
    Your idea has its merits, but enforcing at() isn’t enough. You also have to enforce handling the out_of_range exception. Otherwise you’ve just transformed UB into std::terminate(). That’s an improvement, but still only a better kind of bad.
    – besc
    Jun 8 at 7:49
  • 1
    In many cases, out of range is caused by a programming error - the goal here is to more easily find the bugs, since stack overflows are notoriously hard to track sometimes. So in this case, the program should abort anyway (hopefully with some useful information, like a stack trace). It seems to me that std::terminate() is not that bad.
    – Paul92
    Jun 8 at 11:44
5

You already identified the downside: Inefficiency.

That is not so much due to the exception potentially involved, as most modern implementations of C++ remove all exception-handling out of the normal path.
Rather, changing the Undefined Behavior which can be taken advantage of in myriad ways by the compiler to throwing involves a test and rarely taken conditional branch.

The compiler cannot always prove what the programmer would know, be it due to lacking knowledge of the context, or lack of resources to work it out.

Also, code using .at() might come to rely on the exception, which might make it even less efficient, as the compiler thereafter cannot optimize out the check for sure.

There are containers which have significantly different semantics for .at() and []:

  • std::string. Specifically, the terminator is not part of the sequence for those operations following .at(), but is part of the sequence for those following [].
  • std::map and other associative containers. Specifically, .at() only accesses existing elements, while [] creates new entries as needed.

So, you can enforce it for new code if the loss of efficiency is acceptable and the modified semantics fit, but don't try automatically transforming existing code.
Even manual transformation is unlikely to be bug-free as it only sometimes matters.

2
  • Interesting point, that adding .at() is not only adding a check, but adding all the inefficiencies due to exceptions. Are you aware of any metrics on this? How bad is the impact?
    – Paul92
    Jun 8 at 11:45
  • @Paul92 It isn't so much inefficiency due to the possibility of exceptions (exception-handling in most C++ implementations is designed for pretty much zero cost if none thrown), but the cost of checking at all. [] out-of-bounds for all but associative containers means UB, so compiler just assumes never happens, while .at() out-of-bounds means the check triggers an exception. -and zero cost exception handling for non-exception path still means extra code and data to handle it exist. Jun 8 at 11:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.