In this section, we provide some guidelines and examples for applying such C++ language constructs as:
8.2.1 Explicit Keyword UsageIn the image framework, we use the explicit keyword to prevent undesired conversions. Usually, you want the compiler to automatically convert an object from one type to another. Whenever you see an object of one type being compared or manipulated with an object of another type, you know there are two things that can be occurring:
At times, it is undesirable for the compiler to implicitly convert an object from one type to another. If the compiler finds a way to perform an implicit conversion, it will do so, regardless of whether or not the conversion is correct. After all, the compiler does not understand the specific meaning of a constructor, only that it can be used to perform a conversion. Let's look at an example where using explicit can address these issues.
During Template InstantiationAnother place where explicit can remove confusion is during template instantiation. When you define a template object, you create a generic template that can be used for any data type. It is not uncommon to create a template object with the intention of using only a few specific data types. A problem can occur when a new data type is used for the first time. You can use a technique, like we do, of letting the compiler tell you what functionality is missing that prevents a template instantiation from working. The compiler will try to do simple conversions automatically, and it is possible that it will successfully create an instantiation by applying the incorrect conversions. If you do have a single argument constructor, or a constructor that the compiler will treat as if it had a single argument, you should review whether or not the compiler is making the appropriate conversion. To ensure that it does, use the explicit keyword. If the compiler finds that you are attempting this conversion, and this is your intention, explicitly call the constructor rather than removing the explicit keyword. 8.2.2 Const UsageWe sometimes get sloppy with const during prototyping and in unit tests. Before releasing our software, we go back to the unit tests and change the code, such that any object that is not altered by a method is defined as const . We define a method as being const if it looks like a const object to the client (i.e., the code using this object). The compiler enforces that there can be no changes to the const object.
apTest is a simple repository for an integer, int , accessed by means of get() and set() methods . However, the object also keeps track of the number of times that get() is called. From the standpoint of the code that uses apTest , the get() method is constant. However, in this example there is an internal variable, used for debugging or perhaps caching, that is changing. Because of this, the compiler will not allow you to define this function as const . This is where the mutable keyword is used. The mutable keyword defines a variable that can be changed inside a const function. This is exactly what we are trying to do. All we have to do is define counter_ as: mutable int counter_; and the compiler accepts the function. This is much better than the alternative, before mutable was available, as shown: int get () const { apTest* obj = const_cast<apTest*>(this); // Get non-const pointer obj->counter_++; return value_; } In addition, if you didn't have the const_cast<> operator, you would be forced to write this line of code as shown: apTest* obj = (apTest*)this; Clearly, using mutable is the preferred solution for allowing changes to a variable inside a const function. GuidelinesHere are simple guidelines for using const :
8.2.3 Pass by Reference UsageWe saw in the last section that the use of const and passing by reference are often used together. References can be used in numerous ways, both as return values and arguments. There are some combinations, however, that should be used sparingly. In this section, we review all of the combinations and provide guidelines for their use. Returning a const ReferenceThis is one of the most common uses for references. By making the return value const , you can safely return object variables without copying them. Copies should be avoided whenever possible, even if that object uses reference counting, like our image storage class does. Returning a Non-const referenceReturning a non- const reference to an internal variable gives full access, without restriction, to that variable. By design, returning a non- const reference is done in the assignment operator ( operator= ), prefix increment ( operator++ ), and prefix decrement ( operator-- ), in addition to other places in the language. You should be very careful when using this feature, as it is easy to misuse the reference that is returned. If you see this construct in your code, you should redesign it, as follows. If you are returning a reference because an external function needs to access it, consider using friendship instead. Obviously, you cannot return a non- const reference in a const function. In general, if you are returning a non- const reference, it is most likely a bug in your code unless you have deliberately and carefully applied this construct. If you do use non- const references, be sure to comment it in your code so that others are clear about your intentions. Passing by const ReferenceThis is also a very common use for references. Arguments passed by const reference avoid the copy issue. It also helps the function prototype become self-documenting . For example, the duplicate() functionality in our image framework looks like this: template<class T1, class S1> apImage<T1,S1> duplicate (const apImage<T1,S1>& src, apRectImageStorage::eAlignment align); The const reference clearly indicates that this is a source image as opposed to an output image. Passing by ReferencePassing by reference is a perfectly acceptable practice. Usually when you see an argument passed by reference (not const reference), you can assume it is used as a return value. When you have more than one output parameter, you can either return one parameter normally with the rest returned by setting reference arguments, or you can return a structure. We use both types, depending upon the application. If you only have two pieces of information to return, returning a std::pair<> is a great way of handling this. During Template InstantiationWe frequently use references to assist the compiler during template instantiation. By using references to pass the destination information, the proper template instantiation can occur, as shown in the following example. We chose to use a slightly different definition, but we could have written our add2() functions as: template<class T1, class T2, class T3> void add2 (const T1& s1, const T2& s2, T3& d1) { d1 = s1 + s2;} Returning Multiple ValuesThe other use of references is for returning multiple values. For example, you might have a function like: bool getValue (int index, std::string& value); getValue() returns the status of the operation, and if successful, value is set to the appropriate string. For functions that only return two values, we find this type of construct acceptable. However, if you have additional results, you should create a structure to hold all the results, as shown. struct results { bool status; std::string value; // other results }; results getValue (int index); The Boost Graph library has an interesting construct, called tie() , that allows you to treat the values of a std::pair<> as two separate pieces of data. See [Siek02]. Using this construct, you can write: std::pair<int, float> someFunction(); int i; float f; tie(i, f) = someFunction(); |