C++: Vector bounds
Asked Answered
N

4

16

I am coming from Java and learning C++ in the moment. I am using Stroustrup's Progamming Principles and Practice of Using C++. I am working with vectors now. On page 117 he says that accessing a non-existant element of a vector will cause a runtime error (same in Java, index out of bounds). I am using the MinGW compiler and when I compile and run this code:

#include <iostream>
#include <cstdio>
#include <vector>

int main() 
{ 
    std::vector<int> v(6);
    v[8] = 10;
    std::cout << v[8];
    return 0;
}

It gives me as output 10. Even more interesting is that if I do not modify the non-existent vector element (I just print it expecting a runtime error or at least a default value) it prints some large integers. So... is Stroustrup wrong, or does GCC have some strange ways of compiling C++?

Narcotic answered 23/12, 2012 at 23:48 Comment(4)
Use v.at(8) and you will get a runtime exception.Rationality
Try a quick peek at std::vector<>'s array operator and it may shed some light on your question, specifically the lack of boundary checking (which member at() does do). Your code is UB as-is.Waybill
@Waybill What does UB mean? Sorry for the stupid question, but I am a newb :(Narcotic
@lekroif: "Undefined behavior", i.e. anything can happen, including it doing what you expected.Balough
L
20

The book is a bit vague. It's not as much a "runtime error" as it is undefined behaviour which manifests at runtime. This means that anything could happen. But the error is strictly with you, not with the program execution, and it is in fact impossible and non sensible to even talk about the execution of a program with undefined behaviour.

There is nothing in C++ that protects you against programming errors, quite unlike in Java.


As @sftrabbit says, std::vector has an alternative interface, .at(), which always gives a correct program (though it may throw exceptions), and consequently one which one can reason about.


Let me repeat the point with an example, because I believe this is an important fundamental aspect of C++. Suppose we're reading an integer from the user:

int read_int()
{
    std::cout << "Please enter a number: ";
    int n;
    return (std::cin >> n) ? n : 18;
}

Now consider the following three programs:

The dangerous one: The correctness of this program depends on the user input! It is not necessarily incorrect, but it is unsafe (to the point where I would call it broken).

int main()
{
    int n = read_int();
    int k = read_int();
    std::vector<int> v(n);
    return v[k];
}

Unconditionally correct: No matter what the user enters, we know how this program behaves.

int main() try
{
    int n = read_int();
    int k = read_int();
    std::vector<int> v(n);
    return v.at(k);
}
catch (...)
{
    return 0;
}

The sane one: The above version with .at() is awkward. Better to check and provide feedback. Because we perform dynamic checking, the unchecked vector access is actually guaranteed to be fine.

int main()
{
    int n = read_int();

    if (n <= 0) { std::cout << "Bad container size!\n"; return 0; }

    int k = read_int();

    if (k < 0 || k >= n)  { std::cout << "Bad index!\n"; return 0; }

    std::vector<int> v(n);
    return v[k];
}

(We're ignoring the possibility that the vector construction might throw an exception of its own.)

The moral is that many operations in C++ are unsafe and only conditionally correct, but it is expected of the programmer that you make the necessary checks ahead of time. The language doesn't do it for you, and so you don't pay for it, but you have to remember to do it. The idea is that you need to handle the error conditions anyway, and so rather than enforcing an expensive, non-specific operation at the library or language level, the responsibility is left to the programmer, who is in a better position to integrate the checking into the code that needs to be written anyway.

If I wanted to be facetious, I would contrast this approach to Python, which allows you to write incredibly short and correct programs, without any user-written error handling at all. The flip side is that any attempt to use such a program that deviates only slightly from what the programmer intended leaves you with a non-specific, hard-to-read exception and stack trace and little guidance on what you should have done better. You're not forced to write any error handling, and often no error handling ends up being written. (I can't quite contrast C++ with Java, because while Java is generally safe, I have yet to see a short Java program.)</rantmode>

Lakeesha answered 23/12, 2012 at 23:51 Comment(3)
Worth mentioning std::vector::at?Press
Your example uses operator[] which according to cplusplus.com is NOT bounds checking, so try/catch won't work. [Unless I'm reading the cplusplus.com wrong!] I do agree that the BEST thing is to do the checking in your code - that also helps in the sense you can check a value in the beginning of a function [for example] and then rely on the fact that my for loop or similar works from 0 .. n, and I've checked that n is within range, so lower numbers should be OK [check should of course check that n >= 0!Spiroid
And of course, one part of why C++ is so efficient [if used correctly] is just because it doesn't scatter a bunch of extra checking all over the place "just in case". This was the orignal goal from Bjarne when he created the language - it should "as efficient as C, but as flexible and object oriented as Simula".Spiroid
M
7

This is a valuable comment by @Evgeny Sergeev that I promote to the answer:

For GCC, you can -D_GLIBCXX_DEBUG to replace standard containers with safe implementations. More recently, this now also seems to work with std::array. More info here: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html

I would add, it is also possible to bundle individual "safe" versions of vector and other utility classes by using gnu_debug:: namespace prefix rather than std::.

In other words, do not re-invent the wheel, array checks are available at least with GCC.

Matronage answered 2/2, 2018 at 12:20 Comment(0)
S
6

C and C++ does not always do bounds checks. It MAY cause a runtime error. And if you were to overdo your number by enough, say 10000 or so, it's almost certain to cause a problem.

You can also use vector.at(10), which definitely should give you an exception. see: http://www.cplusplus.com/reference/vector/vector/at/ compared with: http://www.cplusplus.com/reference/vector/vector/operator%5B%5D/

Spiroid answered 23/12, 2012 at 23:53 Comment(3)
operator[] doesn't do a a boundary check. This bit is true, but to be honest I don't think it will ever cause a runtime error unless you try to access a really huge index that is out of memory boundCarrier
If you run off the stack by enough, you'll crash the program. That's a "runtime error" of sorts. But yes, as the other answer says, the definition of "error" here is pretty vague - it's definitely "not going to work reliably", but what actually happens is not clear (called "Undefined behaviour")Spiroid
Undefined behaviour is even worse, than a runtime error. Sometimes it works, sometimes it doesn't ...Paulitapaulk
F
4

I hoped that vector's "operator[]" would check boundary as "at()" does, because I'm not so careful. :-)

One way would inherit vector class and override operator[] to call at() so that one can use more readable "[]" and no need to replace all "[]" to "at()". You can also define the inherited vector (ex:safer_vector) as normal vector. The code will be like this(in C++11, llvm3.5 of Xcode 5).

#include <vector>

using namespace std;

template <class _Tp, class _Allocator = allocator<_Tp> >
class safer_vector:public vector<_Tp, _Allocator>{
private:
    typedef __vector_base<_Tp, _Allocator>           __base;
public:
    typedef _Tp                                      value_type;
    typedef _Allocator                               allocator_type;
    typedef typename __base::reference               reference;
    typedef typename __base::const_reference         const_reference;
    typedef typename __base::size_type               size_type;
public:

    reference operator[](size_type __n){
        return this->at(__n);
    };

    safer_vector(_Tp val):vector<_Tp, _Allocator>(val){;};
    safer_vector(_Tp val, const_reference __x):vector<_Tp, _Allocator>(val,__x){;};
    safer_vector(initializer_list<value_type> __il):vector<_Tp, _Allocator>(__il){;}
    template <class _Iterator>
    safer_vector(_Iterator __first, _Iterator __last):vector<_Tp,_Allocator>(__first, __last){;};
    // If C++11 Constructor inheritence is supported
    // using vector<_Tp, _Allocator>::vector;
};
#define safer_vector vector
Fishback answered 29/11, 2013 at 14:35 Comment(2)
For GCC, you can -D_GLIBCXX_DEBUG to replace standard containers with safe implementations. More recently, this now also seems to work with std::array. More info here: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.htmlGeorgiannegeorgic
Thanks! Yes, it looks working on libstdc++ well! Although, it does not work on libc++ that is default C++ library on Mac OS X...Fishback

© 2022 - 2024 — McMap. All rights reserved.