Use const whenever possible

The const keyword allows you to specify a semantic constraint: an object should not be modified. And compilers will enforce that constraint. This will allow you to communicate to the compiler and to other developers that the object should remain invariant. As a result you code may become more robust, less error prone and even faster as the compiler may become more options for optimizations. Therefore, whenever possible, use the const keyword. What means „whenever possible“? Of course, as developer you know why you implement a method or a variable. So you know whether you need to change the value of a member or if it remains unchanged. And whenever you don not have to change a value you have to declare it as const. For example most method parameters are used as input only and therefore they remain unchanged and can be set to constant.
Outside of classes, you can use the const keyword at global or namespace scope, as well as for objects declared static at file, function, or block scope. Inside of classes, you can use it for both static and non-static data members. That means whenever you declare a variable you can set their behavior to const and therefore mark it as invariant.

Const pointers
Whenever you declare a pointer you have to keep in mind that two kinds of variables are involved. First you have some kind of data and second you have the pointer itself. And each of both elements can be variable or constant. Therefor you have the possibility to define the pointer and/or the data as constant:

char data[] = "Hello world"; 
char* p = data;               // non-const pointer, non-const data
const char* p = data;         // non-const pointer, const data
char* const p = data;         // const pointer, non-const data
const char* const p = data;   // const pointer, const data

Sometimes this syntax is hard to read, especially if you are new to C++ development. To make it easier to understand you can read such a pointer declaration from right to left. For example the third declaration above can be read as: you have a constant pointer p which points to a char.

Const iterators
Iterators are pointers too. So we can expect the same possibilities as above. And in fact you have the same possibilities. You can set whether the data and/or the iterator itself is constant or not. The following example shows the four possible combinations of const/non-const data and pointer in case we using an iterator for a vector.

std::vector<int> data{ 1, 2, 3 };

// non-const iterator, non-const data
std::vector<int>::iterator iter = data.begin();
*iter = 10;   // OK
++iter;       // OK

// const iterator, non-const data
const std::vector<int>::iterator iter = data.begin();   
*iter = 10;   // OK
++iter;       // error

// non-const iterator, const data
std::vector<int>::const_iterator iter = data.begin();   
*iter = 10;   // error
++iter;       // OK

// const iterator, const data
const std::vector<int>::const_iterator iter = data.begin();
*iter = 10;   // error
++iter;       // error

The iterator is a good example for the usage of const. Often you implement a loop over your container data to make data evaluations. In these cases you don’t want to change the data itself and therefore you can use “const_iterator”. But in source code I only see this type of iterator very rarely. So that is a good example why every time you declare a variable you should think about their variability.

Const Member Functions
A member functions marked as const can only be invoked at const objects. Such member functions are important because to work with const objects is a fundamental way to create efficient code. Furthermore, some people argue, that such functions make the interface of the object easier to understand. That’s true as you will now know the function does not change the object. But as the main purpose of an object is to hide the internal memory access behind an interface I would argue that such information is not relevant. If I use an object within a client I don’t want to think about the way the object works internally and in good software design I don’t have to think about this topic. The object itself is a black box. Therefore if you implement a const member function you do it because you want to write efficient code.
Typical scenarios for const member functions are getter ore data evaluation functions. The following example shows a class which holds some data and offers a function for data evaluation. This member function can be declared as const.

class MyClass
{
public:  
  bool IsEmpty() const
  {    
    return mData.empty();
  }

private:
  std::string mData;  
};

A fundamental way to improve code performance is to pass objects by reference to const. And const object instances can call const member functions only. That’s one of the reason why const member function can improve you code performance. The following code shows the previous example, extended by a non const member function and function calls. As you can see, a const object instance can call the const member functions only.

class MyClass
{
public:  
  bool IsEmpty() const
  {    
    return mData.empty();
  }

  void ChangeData()
  {
    mData = mData + "x";
  }

private:
  std::string mData;  
};

int _tmain(int argc, _TCHAR* argv[])
{
  MyClass nonConstInstance;
  const MyClass constInstance;

  nonConstInstance.ChangeData();
  nonConstInstance.IsEmpty();

  constInstance.ChangeData();   // error
  constInstance.IsEmpty();

  return 0;
}

But what does const mean? C++ has the concept of bitwise constness. Therefore compilers will check whether a const member function changes the instance data. Let us test this behavior by changing the source code of the above example and add another member function. We want to count how often the member function is called. So we add a member to store this counter and increase it on function call.

class MyClass
{
public:
  bool IsEmpty() const
  {
    mCounter++;   // error
    return mData.empty();
  }

private:
  std::string mData;
  int mCounter;
};

The compiler will check for bitwise constness and therefore an error is shown as the member mCounter cannot be modified. If we explicitly implement such functionality we can declare the according members as mutable. Mutable members can even be changed in const member functions and therefore they are not included in the bitwise constness check. The following source code shows an according implementation of the previous example.

class MyClass
{
public:
  bool IsEmpty() const
  {
    mCounter++;   // error
    return mData.empty();
  }

private:
  std::string mData;
  mutable int mCounter;
};

The possibility of mutable members speaks for my argument that a client should not think about the internal behavior of an object. Like explained previously some people argue that they like to see const functions in the object interface because they now know that the objects is not changed on such a function call. But as we have seen now, this expectation may not be true.

To make it even more complicate, there is another philosophy behind the constness of an object instance. In contrast to the bitwise constness implemented in C++ you can also think in terms of logical constness. I think it is important to think about these two perspectives. Logical constness means that the object instance is not changed from a logical point of view. As this point of view is always situational if cannot be checked by the compiler but it is an important development guideline. We have seen an according example above. Our class contains members which we explicitly want to change. So our object instance is no longer bitwise const and we explicitly have to disable this check for the according members. But from a logical point of view the object instance stays const as we only have changed some statistical data, maybe needed for debugging or code improvement reasons. Another often discussed aspect are pointers. If your calls contains pointers, you can modify the data referenced by the pointer within a const member function. As you don’t change the pointer itself the bitwise constness rule is not affected. But from a logical point of view this may have a huge impact as you change some base data of the object instance.

In summary const member functions should be used whenever possible as they will allow writing efficient code. For example a fundamental way to improve code performance is to pass objects be reference to const. And const member functions fit seamlessly into this concept. The widespread opinion that const member functions guarantee the constness of the object instance should be seen with caution. As we have seen above there are two kinds of constness. So some people speak about bitwise constness and some about logical constness. And none of these concepts will guarantee that the data of the object is not changed. Therefore the concept of object instance constness should be respected in programming concepts and guidelines. As client developer you only know the interface of the object and you should therefore not make any assumption about the object constness.

Advertisements
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s