• No results found

A Fly in the Ointment?

In document More Exceptional C++ pdf (Page 47-49)

I didn't just pull Example 7-1(c) out of a hat. It turns out there are a lot of real-world C++

programmers who have done exactly this, and with good reason, because code like that in Example 7- 1(c) illustrates some of the important benefits that come from using a

vector

:

• We don't have to know the potential size of

c

in advance. In many C programs, arrays are allocated pessimistically so that they'll be big enough for most intended uses. Unfortunately, this means that space is needlessly wasted much of the time when the full size isn't needed, after all. (Worse, it means the program breaks if it ever ends up needing more space than we'd thought.) With a C++

vector

, we can grow the container as needed instead of just guessing. Also, if we do happen to know in advance how many objects we're going to put into the

vector

, we can simply say so (by first calling

c

.

reserve(100)

, if we want to reserve space for 100 elements), so there's no performance disadvantage with a

vector

compared to an array, because we have a way to avoid repeated reallocations as we insert new elements into the

vector

.

• We don't have to separately track the actual length of the array so that we can pass it as the final (

nLength

) parameter to

FindCustomer()

. Yes, you can use a C array and use workarounds that make remembering or recalculating the length easier,[12] but none of those

workarounds are as easy and safe as just writing

c

.

size()

.

[12] The most typical approaches are to use a

const

value or a

#defined

name for the size of

the array, or else a macro that expands to

sizeof(c)/sizeof(c[0])

to compute the size of the array.

In short, as far as I know, Example 7-1(c) has always worked on every implementation of

std::vector

that has ever existed.

Until 2001, a minor fly in the ointment was that the 1998 C++ standard [C++98] didn't actually guarantee that Example 7-1(c) would work portably on every C++ platform you might buy. This is because Example 7-1(c) assumes that the internal contents of the

vector

are stored contiguously in the same format as is an array, but the original C++ standard didn't require vendors to implement

contents in some other way (such as contiguously but in backwards order, or in sequential order but with an extra byte of housekeeping information between elements), then Example 7-1(c) would, in fact, fail miserably. This was fixed in 2001 as part of Technical Corrigendum #1 to the C++ standard, which now requires that the contents of a

vector

are stored contiguously, just like an array. Even if your compiler doesn't include support for the Technical Corrigendum yet, though, Example 7- 1(c) should work in practice today. Again, I don't know of a commercial

vector

implementation that doesn't store its elements contiguously.

Bottom line: Prefer using

vector

(or

deque

; see below) instead of arrays.

In Most Cases, Prefer Using vector

1. In the standard library,

vector

and

deque

provide similar services. Which should you typically use? Why? Under what circumstances would you use the other?

The C++ standard [C++98], in section 23.1.1, offers some advice on which containers to prefer. It says:

vector

is the type of sequence that should be used by default. …

deque

is the data structure of choice when most insertions and deletions take place at the beginning or at the end of the sequence.

vector

and

deque

offer nearly identical interfaces and are generally interchangeable.

deque

also offers

push_front()

and

pop_front()

, which

vector

does not. (True,

vector

offers

capacity()

and

reserve()

, which

deque

does not, but that's no loss. Those functions are actually something of a weakness in

vector

, as I'll demonstrate in a moment.) The main structural difference between

vector

and

deque

lies in the way the two containers organize their internal storage. Under the covers, a

deque

allocates its storage in pages, or "chunks," with a fixed number of contained elements in each page. This is why a

deque

is often compared to (and pronounced as) a "deck" of cards,[13] although its name originally came from "double-ended

queue" because of its ability to insert elements efficiently at either end of the sequence. On the other hand, a

vector

allocates a contiguous block of memory, and can only insert elements efficiently at the end of the sequence.

[13] The first use I know of the "deck of cards" analogy for

deque

was by Donald Knuth in [Knuth97].

The paged organization of a

deque

offers several benefits:

• A

deque

offers constant-time

insert()

and

erase()

operations at the front of the container, whereas a

vector

does not, hence the note in the standard about using a

deque

if you need to insert or erase at both ends of the sequence.

• A

deque

uses memory in a more operating system-friendly way. For example, a 10-

megabyte

vector

uses a single 10-megabyte block of memory. On some operating systems that single block can be less efficient in practice than a 10-megabyte

deque

that can fit in a series of smaller blocks of memory. On other operating systems, a contiguous address space can be built of smaller blocks under the covers anyway. On such platforms, this

deque

noncontiguity advantage won't matter.

• A

deque

is somewhat easier to use, and inherently more efficient for growth, than a

vector

. The only operations supplied by

vector

that

deque

doesn't have are

capacity()

and

reserve()

. That's because

deque

doesn't need them! For

vector

, calling

reserve()

before a large number of

push_back()

s can eliminate reallocating ever-larger versions of the same buffer every time it finds out that the current one isn't big enough, after all. A

deque

has no such problem, and having a

deque::reserve()

before a large number of

push_back()

s would not eliminate any allocations (or any other work), because none of the allocations is redundant. The

deque

has to allocate the same number of extra pages whether it does it all at once or as elements are actually appended.

Interestingly, note that within the C++ standard itself, the

stack

adapter prefers

deque

. Even though a

stack

can only grow in one direction and so never needs insertion in the middle or at the other end, the default implementation is required to be a

deque

:

namespace std

{

template<typename T, typename Container = deque<T> >

class stack

{

// ...

};

}

Despite this, for readability and simplicity, prefer using

vector

by default in your programs unless you know you need to efficiently insert and erase at the beginning of the container and don't need the underlying objects to be stored contiguously. Note that

vector

is to C++ what the array is to C— the default container type to use in your programs, and one that does a reasonably good all-around job unless you know you need something different.

The Incredible Shrinking vector

As already noted, it's nice that

vector

manages its own storage, and that we can give it hints about how much capacity to keep ready under the covers as it grows. If we have a

vector<T> c

that we're about to grow to contain 100 elements, we can first call

c

.

reserve(100)

and incur, at most, a single reallocation hit. This allows optimally efficient growth.

But what if we're doing the opposite? What if we're using a

vector

that's pretty big, and then we remove elements we no longer need and want the

vector

to shrink to fit—that is, we want it to get rid of the now-unneeded extra capacity? You might think that the following would work:

2. What does the following code do?

// Example 7-2(a): A naïve attempt at shrinking a vector.

//

In document More Exceptional C++ pdf (Page 47-49)