8.2 Language Construct Issues


In this section, we provide some guidelines and examples for applying such C++ language constructs as:

  • explicit keyword

  • const

  • passing by reference (both const and non- const )

8.2.1 Explicit Keyword Usage

In 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:

  • The compiler is using a defined conversion operator to convert the object from one type to another.

  • The compiler is using a constructor to make the conversion.

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.

graphics/triangle.gif EXAMPLE

Let's create a testString object. This object is nothing but a wrapper around std::string with a single operation, as shown.

 class testString { public:   testString ();   testString (const std::string& s) : string_ (s) {}   testString (const char* s) : string_ (s) {}   testString (int size, const char c=' ') : string_ (size, c) {}   testString operator+ (const testString& src)   {     std::string result = string_ + src.string_;     return result;   }   std::string fetch () const { return string_;} private:   std::string string_; }; 

You can use testString objects in a number of ways:

 testString();                     // An empty string testString("Hello World");        // Contains "Hello World" std::string s("Hello World"); testString(s);                    // Contains "Hello World" testString(10);                   // Contains 10 spaces 

You can also use them to do some useful string manipulations:

 testString a ("Hello "); testString b ("World"); testString c = a + b; testString d = a + "World"; 

c and d both contain "Hello World". The last line is particularly useful to succinctly combine two strings, even though the second string is not a testString object.

With this usefulness , comes some pitfalls. For example, you can also write:

 testString d = a + 5; 

While the intention of this line is completely unclear, the compiler will successfully compile it. This line of code produces the same results as:

 testString spaces (5); testString d = a + spaces; 

d will contain "Hello " . The compiler is able to convert the integer value 5 to a testString because a constructor exists that takes an integer value and a second value as a default. This is where the explicit keyword can help. explicit can be used even when the constructor takes more than one argument, if the arguments have default values. If we modify our class such that the constructor is as follows :

 explicit testString (int size, const char c=' ') : string_ (size, c) {} 

then the compiler will not compile the unclear line of code, instead appropriately generating an error, such as:

 binary '+' : no operator found which takes a right-hand operand of type 'int' (or there is no acceptable conversion) 
During Template Instantiation

Another 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 Usage

We 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.

graphics/triangle.gif EXAMPLE

Let's look at an example where we need to be able to change the value of a variable within a const function.

 class apTest { public:   apTest () : value_ (0), counter_ (0) {}   void set (int v)  { value_ = v;}   int  get () const { counter_++; return value_;}   ... private:   int value_;   int counter_; }; 

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.

Guidelines

Here are simple guidelines for using const :

  • Use const references for any function arguments that do not change. The const portion will tell the compiler to make sure the value is not altered, and the reference will prevent the copy constructor from getting called when the function is called. An example is:

     void set (const std::string& string); void set (const apImage<T>& image); 
  • Examine all functions to see if they can be made const . It is easy to miss them. By labeling a function as const , the compiler becomes responsible for making sure this condition is true. There are some occasions where you will have to make a few changes in order to use const , but it is worth it. For example, suppose you have the following members in a class:

     std::vector<int> v;   int sum () const   {     int s = 0;     std::vector<int>::const_iterator i;     for (i=v.begin(); i!=v.end(); i++)       s += *i;     return s;   } 

    Since the method is const , you must use a const_iterator instead of just an iterator . If you forget, the compiler will remind you. However, in some cases, such as when templates are used, the error messages can be difficult to decipher. See [Meyers01].

  • Return internal values from a const method as const . For example (from apImage<> ):

     const S& storage () const; const T* base    () const; 
  • Add a second method that returns a non- const version when a const method must be accessed by a non- const member. For example (from apImage<> ):

     const T* rowAddress (unsigned int y) const;   T*       rowAddress (unsigned int y); 

8.2.3 Pass by Reference Usage

We 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 Reference

This 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 reference

Returning 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 Reference

This 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 Reference

Passing 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 Instantiation

We 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 Values

The 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(); 


Applied C++
Applied C++: Practical Techniques for Building Better Software
ISBN: 0321108949
EAN: 2147483647
Year: 2003
Pages: 59

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net