Here are some additions to the vector class in C++.
This month, I continue our discussion of a C++ class suitable for doing math with vectors. As useful as this math is, things get even better when we combine matrix and vector operations in a single set of classes. As useful as vector algebra is, it pales into insignificance when compared with the algebra that includes both kinds of arrays. It's my ultimate goal to describe a set of classes to support this algebra, so I'm anxious to complete the vector class.
The canonical set
Whenever I'm starting the development of a new class, I tend to start out with everything–declarations, definitions, and test driver–in a single file. That's a terrible way to write production code, but I like it for development because I don't have to change two or three files every time I change a definition. At this point, though, our class Vector has grown enough to justify a home of its own, in files of its own. I'm organizing those files now and will post them on Embedded.com at a later date. Instead, I want to concentrate on additions to the vector-class member functions.
When I was being taught object-oriented programming, I learned that every class should always contain the four functions:
- Default constructor
- Copy constructor
- Assignment operator
Even for classes that can get by with the compiler-generated forms for the default constructor and destructor, it's a good idea to include those functions.
The copy constructor is a constructor that creates a new copy of an existing object. For the class Vector , it looks like the code shown in Listing 1. The function vCopy( ) is one of the primitives from the file, SimpleVec.cpp . Its purpose should be obvious from the context. Here again, we have an issue of efficiency vs. simplicity and ease of use. Using a function like vCopy , however short it is, may seem extravagant, and perhaps it is, especially since we can accomplish the same thing with the library function, memcpy . I leave it to you to decide how best to implement the action.
The copy constructor isn't the only constructor we can add. We can include converting constructors as we choose, mapping other data types into vectors. In Listing 1, I've also shown two that I find useful: conversions from ordinary arrays and from a set of scalars. The latter constructor–specifically for building three-vectors–is pretty cool. I've given the third element, z , a default value of zero. This means I can use the constructor to manage vectors of only two dimensions. The vector will be converted to a 3-vector, whose third element happens to be zero.
Why is this useful? Well, consider the practical problem of computing the area of a polygon. A polygon is a plane figure, meaning its vertices can be expressed as a set of vectors in the x-y plane. In my October 2006 column (“Motivationally speaking,” p.15) I showed you how we can compute the area as a sum of cross products.
But the cross-product operator is only defined for 3-vectors. The result of a cross product is not really a scalar, but another vector. If the two operands lie in the x-y plane, the resultant cross product ends up along the z -axis. In my career, I can't tell you how many times I've converted a 2-vector to a 3-vector and back again, just to use the cross-product operator somewhere along the way. The pseudo 2-vector generated by the converting constructor handles this operation very neatly. For plane figures, you just have to remember to look for the result in v . . . or is that v(3) ?
Sharp-eyed readers will spot a subtle change. Instead of the type int for indices and counters, I've switched to the much safer size_t . Should have done this sooner.
An assignment operator looks very much like a constructor, and usually shares a lot of common code with the constructor. But its function is very, very different. Where the constructor creates new instances of a given object, the assignment operator copies data from one existing object to another. In Listing 1, I'm including three assignment operators. The default operator copies one vector into another. You'll note that this operator returns a reference to itself. That's so you can do things like:
a = b = c = d;
Also note the important check to make sure we haven't done something dumb like:
a = a;
Without that check, the operator would tell a to free its memory, which is probably not a good thing.The converting operators copy data from one data type into an object of type Vector . As before, you can create as many such operators as you can dream up. I've shown the operator for the same types as in the constructors.The last assignment operator is rather neat. It lets me stuff a scalar value, s, into every element of a vector. There aren't many occasions where you'd like to do this, but the operator works great for:
x = 0;
The next functions I'd like to show you are the functions to perform stream I/O. These functions let you input and output vectors just as you would scalars, strings, or what have you. They're shown in Listing 2.
cin >> a;
cin >> a >> b;
where a and b are vectors. Of course, the same operator works for input from files as well as the console.Note that operator << doesn't write an end-of-line character. You'll have to do that yourself, as usual:
cout << a << endl;
I should explain the strange loop structure in the stream output operator. It's there to avoid writing a space character after the last element. I must admit to you that in most of my implementations, I don't bother with this nicety. Letting the operator write that trailing space works for me, because I rarely string vector outputs together on the same line. Usually, we want to separate them anyhow.
But what if I'm writing to a file? In that case, I may well want to write multiple vectors, all in a row. With the trailing space, I could always write:
cout << a << b << endl;
But that's an unconventional structure. For scalars, we're used to writing things like:
cout << a << ' ' << b << endl;
And we should expect to use the same structure for vectors. The sole purpose of treating the last element specially is to make sure constructs like the one above work as one would expect.Preview of coming attractions
That's about all the code we have time for, this month. What's left is arithmetic operators, and that's where we'll be going next time. Remember that vectors only support four operators: add, subtract, dot product, and cross product. Or, rather, that's all the operators involving two operands (I don't use the term binary in this sense, to avoid confusion). Lots of other operations can be performed, however, including negation (the unary minus), absolute value, converting to a unit vector, and comparison operators. We'll have plenty enough to keep us busy next time.
Before I close, though, I want to remind you that the way we do things depends, in large part, on how much efficiency we're willing to sacrifice, for the sake of ease of use. Consider the two lines of code:
a += b;
c = a + b;
The second line is less efficient because it causes another instance of the class Vector to be created. Even if c already exists, the functioning of the compiler demands the creation–and subsequent destruction–of a temporary. The first line, however, does not.This suggests that we should provide for functions that provide different levels of efficiency. In Table 1, I've tried to summarize a taxonomy.
p = a*b;
I've chosen this operator to represent the dot product. My question to you is, what operator should I use for the cross product? I've already chosen one, but it'll be interesting to see if we agree.
See you next time.
Jack Crenshaw is a senior systems engineer at General Dynamics and the author of Math Toolkit for Real-Time Programming. He holds a PhD in physics from Auburn University. E-mail him at . For more information about Jack