CPSC 324 Fundamentals of Computer Graphics Spring 2006

C++ Notes

The CPSC 324 Renderer package uses a number of features of C++ which you might not be familiar with. This document provides some information about those features. Things covered include:


Pointers vs. (const) Reference

If you want to pass a potentially large something as a parameter to a function, what do you do? You could use call-by-reference e.g.:

void func ( const SomeType & param );

Or, you could pass a pointer to the thing:

void func ( const SomeType * param );

(In both cases, const can be omitted if the function should modify the parameter.)

Neither method copies the object, saving time. So which to use? The CPSC 324 Renderer generally uses (const) reference if inheritance is not involved and pointers if inheritance is involved. For example, consider the render method of Renderer (render/renderer.h):

virtual void render ( const Scene & scene, const LightingModel * lighting,
                      int x, int y, int width, int height ) const = 0;

scene is passed by const reference because the Scene class has no subclasses and is not intended to have any subclasses. On the other hand, LightingModel is subclassed (to provide different lighting models) so lighting is passed as a pointer.

This rule of thumb extends to return values as well - pointers are generally only returned for classes that are or will be subclassed.

The reason for this design decision is based on how late binding (virtual methods) and subclasses are handled in C++ - working with pointers to the objects is much better for avoiding slicing issues (where a subclass object is converted into an instance of a superclass), avoiding problems with abstract classes, and for doing dynamic casting.


const (and const Reference) Return Values

Just as you can pass parameters to functions by value, reference, and const reference, you can return values from functions by value, reference, and const reference.

The most familiar form is return-by-value e.g.:

  string foo ( const string & message ) {
    string response = "the message is: "+message;
    return response;
  }

In this function, the object actually returned by the function is a new string which is a copy of the object response declared in the function.

In some cases, it is desirable (for efficiency or other reasons) to return the actual object rather than a copy. This is (pretty much) only appropriate in class methods where the object being returned is an instance variable. In the example above, it doesn't make sense to return response by reference because response, being a local variable, goes out of scope as soon as the function returns and so the caller of the function now has a reference to something which no longer exists. In classes, however, instance variables persist after a particular method has finished. Reference return values can be (regular) reference or const reference, with essentially the same meaning as for parameters - the caller who receives a const reference return value can't do anything with that value that might change it, such as invoking a non-const method on it.

The CPSC 324 Renderer uses reference and const reference return values in several places for efficiency. For example, the Wireframe class (scene/wireframe.h) has the following method:

    const Segment & getSegment ( int n ) const;

Here the segment returned by getSegment is returned by const reference. For the most part, you'll probably only need to be concerned with (const) reference return values in two ways: recognizing what the syntax means if you see it in a header file, and if you want to store a result returned by (const) reference in a variable (see the next section).


Reference (and const Reference) Variables

Along the lines of reference return values, you can also have reference and const reference variables (though in the latter case it isn't really a "variable" since the value can't change). This is primarily useful in conjunction with functions that return reference or const reference return values, where you want to store the return value in a variable but you want to (again) avoid a copy. For example, the getTriangle method of TriangleMesh (scene/trimesh.h) returns a const Triangle & (that is, a const reference Triangle):

    virtual const Triangle & getTriangle ( int n ) const;

Assume that you have a TriangleMesh * named mesh. You could retrieve the first triangle in the mesh and store it in a variable named tri as follows:

    const Triangle & tri = mesh->getTriangle(0);

tri is then used normally, but since it is const you cannot assign to it or invoke non-const methods. Also, since tri is a reference, it should not be used after the object mesh points to is destroyed.

Your program will still work fine if you don't worry about (const) reference variables e.g.

    Triangle tri = mesh->getTriangle(0);

but you'll lose some efficiency because of the extra copy going on (tri will be a new object which is a copy of the object returned by mesh->getTriangle(0)).


Anonymous Objects

Sometimes you may find yourself creating a variable just so you can pass an object as a parameter or return it from a function. This is fine, but you can short-cut the syntax a bit by using anonymous objects. For example:

Color foo () {
  double red, green, blue;
  // do something to give red, green, and blue values
  Color color(red,green,blue);
  return color;
}

Using anonymous objects, this can be rewritten as:

Color foo () {
  double red, green, blue;
  // do something to give red, green, and blue values
  return Color(red,green,blue);
}

The syntax Color(red,green,blue) creates a new Color object (by calling the constructor with the specified parameters), but doesn't store it in a named variable.


DBL_MAX

The constant DBL_MAX is defined to be the largest possible double value. Don't forget to #include <cfloat> if you use DBL_MAX!


Comparing doubles

Using == to compare two double values has unfortunate problems. Because only a fixed number of bits are used to store numbers, double values can only have a certain number of decimal places. As a result, precision errors can occur - perhaps you've printed out a value that should be 1 but got 0.9999999999 instead, or a value that should be 0 is instead a very small (but non-zero) number.

This issue can cause many problems in computer graphics (unless integer-only computation is used). One fix is to interpret two values which are very close together but not identical as actually being equal. The file util/defs.h defines a value EPSILON to express this "very close" idea - two values within EPSILON of each other should be considered equal. Thus, whenever you want to compare two doubles a and b for equality, you should do one of the following things:

if ( fabs(a-b) < EPSILON ) { ... }

or

if ( a-b < EPSILON && a-b > -EPSILON ) { ... }

fabs returns the absolute value; the second form takes into account that the different might be negative and so accepts anything in the range -EPSILON to EPSILON.


Static Methods

Like Java, C++ has static methods. While normal class methods are invoked on an instance of the class e.g.

  Camera * camera = ...;
  double width = camera->getViewWidth();

static methods aren't object specific and are invoked using only the class name e.g.

  Drawing::setColor(1,1,0);

The presence of the keyword static before the method header in the header file is your only clue as to whether or not a given method is static.

The implementation of static methods is similar to non-static methods, though the static keyword is not present in the header in the implementation file and static methods are not allowed to access class instance variables (because the values of instance variables are specific to a particular instance of the class, and calling static methods doesn't require an instance of the class).


Downcasting

One of the nice things about inheritance and late binding (virtual methods) is that you can often just work with something declared as a superclass type and the "right" version of the methods will be invoked. For example, if you have a variable Camera * camera, you can call camera->getProjSquareUpMatrix() and get the appropriate matrix for a parallel or perspective camera, depending on what kind of object camera is pointing to. This works as long as the operations you want to invoke are declared in the superclass.

Sometimes, however, you want to invoke operations specific to a subclass. For example, you have a variable Light * light but you need to handle ambient lights differently from point lights. In order to satisfy the compiler, you need to downcast light to the actual type (e.g. AmbientLight * or PointLight *).

You may be familiar with using static_cast or the older C-style casting syntax e.g. to cast an integer x to a double: static_cast<double>(x) or (int)x. This is not appropriate for the casting-to-a-subclass type situation, however, because we'd like to also be able to check that the cast is legal (i.e. not all Light * pointers can be cast to AmbientLight * or PointLight *).

The solution is to use dynamic_cast:

PointLight * pointlight = dynamic_cast<PointLight *>(light);

dynamic_cast only works properly with pointer types, which is one of the reasons for always working with pointers to objects instead of the objects themselves when dealing with inheritance. The result of this statement is that pointlight is a new variable pointing to the same object as light, but with a PointLight * "hat". As a result, the compiler is happy with something like pointlight->getPosition() (light->getPosition() would generate a compiler error because the Light class doesn't have a getPosition() method). It is also important to note that dynamic_cast returns NULL if the cast is not legal. So, if light really pointed to an AmbientLight, pointlight would be set to NULL. For robust code, don't forget to check the return value of dynamic_cast before invoking a subclass-specific method.


STL map

Arrays are handy if you want to associate things with integers, and look up the thing based on the integer - myarray[2] give you direct access to the contents of slot 2 in the array myarray. But what if you want to associate things with non-integer values? An associative array provides this functionality, and works in many respects like a regular array (except the array indexes aren't limited to integers).

The C++ Standard Template Library provides the map class to meet needs for associative arrays. (brief tutorial on map and a more complex but thorough listing of map's methods) To use map, you'll need #include <map> and follow that with using namespace std; (so you can write map instead of std::map).

In the CPSC 324 Renderer package, you'll notice map used in the lighting model code - since there is an L vector and an R vector associated with each light source, a convenient way to store these vectors is in an associative array indexed by the light source. For the most part, you'll only need to worry about how to declare map variables and insert and retrieve values - see the brief tutorial on map for examples of declaring and inserting. The tutorial also covers retrieval (unfortunately it isn't as easy as with regular arrays), but the util/utiltemplates.cc file in the support code contains a mapGet function to make retrieval more convenient. With mapGet, retrieving the grade for Tim (the tutorial's example) becomes:

cout << "Tim's grade is: " << mapGet(grade_list,"Tim") << endl;

Note that mapGet assumes there is a value for the key in the map - calling mapGet(grade_list,"Tim") if Tim isn't in the list violates the function's precondition and will result in a failed assertion.


STL stack

Stacks are LIFO data structures - the last thing inserted is the first thing removed, like a stack of trays in a cafeteria. The C++ Standard Template Library provides the stack class to meet needs for stacks. (a complex but thorough listing of stack's methods)

The main things you need to know about most any data structure is how to create one, how to insert, how to remove, and how to find out how many things are contained in it.

stack is a templated class, so the kind of thing stored in the stack is part of the type name:

stack<int> S;           // create an empty stack called S to hold integers

You'll need to #include <stack> to use the stack class.

Inserting into a stack is called pushing:

S.push(5);                  // put 5 on the top of the stack
S.push(10);                 // put 10 on top of the 5, so 10 is now the top

To view the top element without removing it:

cout << S.top() << endl;    // prints 10, because 10 is on top

To remove the top element:

S.pop();                    // now 5 is on top, because 10 was removed

To find out how many things are in the stack:

cout << S.size() << endl;   // prints 1 because only the 5 is left
if ( S.empty() ) {          // true if the size is 0
  cout << "stack is empty" << endl;
}

Valid HTML 4.01!