15

The goal:

I would like to have a range checked version of std::vector's operator [] for my debug builds and no range check in release mode.

The range check in debug mode is obviously good for debugging, but it causes a slowdown of 5% - 10% in my release code which I would like to avoid.

Possible solutions:

I found a solution in Stroustrup's "The C++ programming language". He did the following:

template <class T>
class checked_vector : public std::vector<T> {
    public:
        using std::vector<T>::vector;

        //override operator [] with at()
};

This is problematic because it inherits from a class with non-virtual destructor which is dangerous. (And the Lounge was not too fond of that solution.)

Another idea would be a class like this:

template <class T>
class checked_vector {
    std::vector<T> data_;

    public:
        //put all public methods of std::vector here by hand

};

This would be both tedious and create a large amount of copy-paste which is bad too.

The nice thing about both the above solutions is that I can simply toggle them on and off with a macro definition in my makefile.

The questions:

  1. Is there a better solution? (If not, why not?)
  2. If not, is one of the above considered acceptable? (I know this one is opinion based, please focus on No. 1 if possible.)
Community
  • 1
  • 1
Baum mit Augen
  • 49,044
  • 25
  • 144
  • 182
  • I think Stoustrup's solution was meant to be used in the first couple weeks of teaching for something that just works with minimal hassle (what student is going to be using a vector polymorphically then?) – ghostofstandardspast Jun 16 '14 at 14:32
  • 1
    If you don't need runtime polymorphism, private inheritance might be an option. – juanchopanza Jun 16 '14 at 14:33
  • Would you mind adding the page number? I'd like to read the context. Nevermind, found it in 4.4.1.2 – dyp Jun 16 '14 at 14:41
  • 6
    Have you checked your standard library implementation? MSVC's, for instance, already does this in Debug builds. – Praetorian Jun 16 '14 at 14:44
  • 1
    @Praetorian As does libstdc++, not sure about libc++ – dyp Jun 16 '14 at 14:47
  • @Praetorian I would prefer to toggle the rangecheck myself. – Baum mit Augen Jun 16 '14 at 14:52
  • @juanchopanza Wouldn't I need a `using ...` for every member then? – Baum mit Augen Jun 16 '14 at 14:56
  • @BaummitAugen Only one per name, not per member. So it is tedious, but not as much as your second solution. – juanchopanza Jun 16 '14 at 15:01
  • @juanchopanza Hm, then that beats the first two I guess. – Baum mit Augen Jun 16 '14 at 15:03
  • 1
    Although not guaranteed by the standard, I think non-virtual destructors are safe in practice as long as you don't add any data members and use the default destructor in the derived class. – Mark Ransom Jun 16 '14 at 15:13
  • 3
    A free function might be an option. Something like `template const T& at(const vector& v, int index) { return v.at(index); }` or the function body could look like `return v[index];` if `NDEBUG` is defined. The least amount of hassle, highly likely to be inlined (no overhead). – Ali Jun 16 '14 at 15:38
  • 1
    @Mark Ransom Isn't calling the wrong destructor undefined behavior regardless of whether it appears to work? – Mark B Jun 16 '14 at 15:50
  • While I generally would agree that deriving from a class with a non-virtual destructor is not good practice, do you plan on holding these objects as pointers to the base class (`std::vector*`)? – Chad Jun 16 '14 at 15:52
  • @MarkB yes, that's why I started my comment the way I did. The real answer of course is to never use such a class polymorphically. I'm just saying you're unlikely to get in trouble even if you screw up. – Mark Ransom Jun 16 '14 at 15:53
  • Note that the comments you linked to don't say anything about the lack of a virtual destructor (indeed, you had never even mentioned it at this point), so your use of the word "thus" in the question is misleading. – Lightness Races in Orbit Jun 16 '14 at 16:22
  • 3
    The Lounge is not known for its "fondness of things". – Tony The Lion Jun 16 '14 at 16:25

2 Answers2

16

If I'm not mistaken, this is the usual situation with Visual Studio. With g++, you have to invoke the compiler with -D_GLIBCXX_CONCEPT_CHECKS -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC. (It's probable that you don't need all three, but I use all three systematically.) With other compilers, check the documentation. The purpose of the undefined behavior in the standard here is precisely to allow this sort of thing.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • With Clang + libc++, there was an experimental undertaking (`_LIBCPP_DEBUG`), however it did not pan out, and with the advent of ASan and the like it may be low-priority now. – Matthieu M. Jun 16 '14 at 16:09
4

In decreasing order of preference:

  • If you utilize iterators, ranges, and iteration rather than indexes into the container, the problem just goes away because you are no longer passing in arbitrary indexes that need to be checked. At that point you may decide to replace any remaining code that requires indexes with at instead of using a special container.

  • Extend with algorithms rather than inheritance as suggested in one of the comments. This will almost certainly be completely inlined away and is consistent with the standard using algorithms rather than additional member functions. It also has the advantage of working with any container that has operator[] and at (so it would also work on deque):

    template <typename Container>
    const typename Container::value_type& element_at(const Container& c, int index)
    {
        // Do checked code here.
    }
    
  • Privately inherit std::vector and using the methods you need into your child class. Then at least you can't improperly polomorphically destroy your child vector.

Mark B
  • 95,107
  • 10
  • 109
  • 188
  • 2
    Note: if you use iterators, you might accidentally use invalidated ones though, so it's not exactly a panacea. – Matthieu M. Jun 16 '14 at 16:04
  • I would put into the first position: *Use whatever debugging facility is provided with your Standard Library implementation. Both gcc and MSVC already provide what you need.* – Ali Jun 16 '14 at 16:05
  • Iterators do not solve the problem. Ranges do, but iterators themselves don't. – Mooing Duck Jun 16 '14 at 16:41
  • @MooingDuck Are you saying that ranges cannot be invalidated by operations on the underlying container? – D Drmmr Jun 16 '14 at 18:13
  • @DDrmmr: Hah! True. I was thinking read-only ranges, but realistically there's holes even there. – Mooing Duck Jun 16 '14 at 18:25