============================================================================== C-Scene Issue #2 Const Correctness in C++ Chad Loder ==============================================================================
In C, you merely shoot yourself in the foot.While it is true that using the object-oriented features of C++ requires more thought (and hence, more opportunities to make mistakes), the language provides features that can help you create more robust and bug-free applications. One of these features is const, the use of which I will address in this article.In C++, you accidentally create a dozen instances of yourself and shoot them all in the foot. Providing emergency medical care is impossible, because you can't tell which are bitwise copies and which are just pointing at others and saying, "That's me, over there."
Used properly with classes, const augments data-hiding and encapsulation to provide full compile-time safety; violations of const cause compile-time errors, which can save you a lot of grief (from side-effects and other accidental modifications of data). Some C++ programmers believe const-correctness is a waste of time. I disagree - while it takes time to use const, the benefits almost always outweigh the time spent debugging. Furthermore, using const requires you to think about your code and its possible applications in more detail, which is a good thing. When you get used to writing const-correctly, it takes less time - this is a sign that you have achieved a state of enlightenment. Hopefully this article will help put you on the path of eternal bliss.
int x = 4; // a normal variable that can be modified x = 10; // legal const int x = 2; // const var can be initialized, not modified thereafter x = 10; // error - cannot modify const variable
The const keyword is more involved when used with pointers. A pointer is itself a variable which holds a memory address of another variable - it can be used as a "handle" to the variable whose address it holds. Note that there is a difference between "a read-only handle to a changeable variable" and a "changeable handle to a read-only variable".
const int x; // constant int x = 2; // illegal - can't modify x const int* pX; // changeable pointer to constant int *pX = 3; // illegal - can't use pX to modify an int pX = &someOtherIntVar; // legal - pX can point somewhere else int* const pY; // constant pointer to changeable int *pY = 4; // legal - can use pY to modify an int pY = &someOtherIntVar; // illegal - can't make pY point anywhere else const int* const pZ; // const pointer to const int *pZ = 5; // illegal - can't use pZ to modify an int pZ = &someOtherIntVar; // illegal - can't make pZ point anywhere else
int y; const int* pConstY = &y; // legal - but can't use pConstY to modify y int* pMutableY = &y; // legal - can use pMutableY to modify y *pMutableY = 42;
C++ does not allow you to circumvent const easily because the assignment operator can't be used to put the contents of a const int* into a normal int* without explicit casts. C++ does not supply a standard conversion from a const type to a type that is not const. However, any sort of conversion can be specified with explicit type casts (including unsafe conversions). Thus, the type-system in C++ generally will not allow you to put the address of const data into a pointer to non-const data.
For example, try to put the address of x, which is const, into a normal int* so you can use it to modify the data:
const int x; // x cannot be modified const int* pX = &x; // pX is the address of a const int // and can't be used to change an int *pX = 4; // illegal - can't use pX to change an int int* pInt; // address of normal int pInt = pX; // illegal - cannot convert from const int* to int*
int *pInt; // address of a normal int pInt = &x; // illegal - cannot convert from const int* to int*
The const keyword can't keep you from purposely shooting yourself in the foot. Using explicit type-casting, you can freely blow off your entire leg, because while the compiler helps prevent accidental errors, it lets you make errors on purpose. Casting allows you to "pretend" that a variable is a different type. For example, C programmers learn early on that the result of dividing an integer by an integer is always an integer:
int x = 37; int y = 8; double quotient = x / y; // classic mistake, result is rounded to an int cout << quotient; // prints " 4.000000" double quotient = (double)x/y; // cast result as double so it's not rounded cout << quotient; // prints "4.625000"
The following code is a good illustration of how to mess yourself up with forced casting:
const int x = 4; // x is const, it can't be modified const int* pX = &x; // you can't modify x through the pX pointer cout << x << endl; // prints "4" int* pX2 = (int *)pX; // explicitly cast pX as an int* *pX2 = 3; // result is undefined cout << x << endl; // who knows what it prints?
However, when you look at it more closely, strange things are happening. When you run the code, the output (from cout or printf) seems to show that x doesn't change in the second assignment. But when you step through the code, the debugger shows you that x does, in fact, change. So what is happening? If x changes, then why doesn't the output statement reflect this change?
Often in such bizarre situations, it is a good idea to look at the assembler code that was produced. In Visual C++, compile with the /Fa"filename.asm" option to output the assembler with the corresponding lines of code into a file so you can look at it. Don't panic if you don't know much about assembler - if you know how arguments are pushed onto the stack, it's really quite easy to see what's happening.
ASSEMBLER OUTPUT C++ CODE Mov eax, DWORD PTR _pX$[ebp] int* pX2 = (int *)pX; Mov DWORD PTR _pXX$[ebp], eax Mov eax, DWORD PTR _pXX$[ebp] *pX2 = 3; Mov DWORD PTR [eax], 3 Push OFFSET FLAT:?endl@@......... cout << x << endl; Push 4
const int x = 4; // x is const, it can't be modified const int* pX = &x; // you can't modify x through the pX pointer cout << x << endl; // prints "4" int* pX2 = const_cast < int* > (pX); // explicitly cast pX as non-const *pX2 = 3; // result is undefined cout << x << endl; // who knows what it prints?
The C++ standard (section lex.string) states:
In the following example, the compiler automatically puts a null-character at the end of the literal string of characters "Hello world". It then creates a storage space for the resulting string - this is an array of const chars. Then it puts the starting address of this array into the szMyString variable. We will try to modify this string (wherever it is stored) by accessing it via an index into szMyString. This is a Bad Thing; the standard does not say where the compiler puts literal strings. They can go anywhere, possibly in some place in memory that you shouldn't be modifying.1 A string literal is a sequence of characters (as defined in _lex.ccon_) surrounded by double quotes, optionally beginning with the letter L, as in "..." or L"...". A string literal that does not begin with L is an ordinary string literal, also referred to as a narrow string literal. An ordinary string literal has type "array of n const char" and static storage duration (_basic.stc_), where n is the size of the string as defined below, and is initialized with the given characters. A string literal that begins with L, such as L"asdf", is a wide string literal. A wide string literal has type "array of n const wchar_t" and has static storage duration, where n is the size of the string as defined below, and is initialized with the given charac- ters. 2 Whether all string literals are distinct (that is, are stored in nonoverlapping objects) is implementation-defined. The effect of attempting to modify a string literal is undefined.
char* szMyString = "Hello world."; szMyString[3] = 'q'; // undefined, modifying static buffer!!!
char *const a = "example 1"; // a const pointer to (he claims) non-const data a[8] = '2'; // Coplien says this is OK, but it's actually undefined
If you've been paying attention, you'll remember that the type-system in C++ will not allow you to put the address of const data into a pointer to non-const data without using explicit type casts, because there is no standard conversion between const types and types that are not const. Example:
const char constArray[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; char nonConstArray[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; char* pArray = constArray; // illegal char* pArray = nonConstArray; // legal
// should be illegal - converts array of 6 const char to char* char* pArray = "Hello";
Notice item 2 in the above quote from the language standard: literal strings don't have to be distinct. This means that it is legal for implementations to use string pooling, where all equal string literals are stored at the same place. For example, the help in Visual C++ states:
"The /GF option causes the compiler to pool strings and place them in read-only memory. By placing the strings in read-only memory, the operating system does not need to swap that portion of memory. Instead, it can read the strings back from the image file. Strings placed in read-only memory cannot be modified; if you try to modify them, you will see an Application Error dialog box. The /GF option is comparable to the /Gf option, except that /Gf does not place the strings in read-only memory. When using the /Gf option, your program must not write over pooled strings. Also, if you use identical strings to allocate string buffers, the /Gf option pools the strings. Thus, what was intended as multiple pointers to multiple buffers ends up as multiple pointers to a single buffer."To test this, you can write a simple program as follows:
#include <stdio.h> int main() { char* szFirst = "Literal String"; char* szSecond = "Literal String"; szFirst[3] = 'q'; printf("szFirst (%s) is at %d, szSecond (%s) is at %d\n", szFirst, szFirst, szSecond, szSecond); return 0; }
szFirst (Litqral String) is at 4266616, szSecond (Litqral String) is at 4266616Sure enough. Although there was only one change, since string pooling was activated, both char* variables pointed to the same buffer. The output reflects this.
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; private: char* m_szName; };
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; void PrintName() { cout << m_szName << endl; }; private: char* m_szName; };
So, we can do something like the following:
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; void GetName(char *szBuf, const size_t nBufLen) { // ensure null termination in the copy strncpy(szBuf, m_szName, nBufLen - 1); }; private: char* m_szName; };
Person P("Fred Jones"); char szTheName = new char[256]; P.GetName(szTheName, 256); cout << szTheName << endl;
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; char* GetName() { return m_szName; }; private: char* m_szName; };
Person P("Fred Jones"); cout << P.GetName() << endl;
// this function overwrites szString // (which may have held the address of dynamically allocated memory) void MyBuggyPrint(char* szString) { // make a copy of the string and print out the copy szString = _strdup(szString); cout << szString << endl; free (szString); } Person P("Fred Jones"); MyBuggyPrint(P.GetName());
Fortunately, the const keyword comes in handy in situations like this. At this point, I'm sure some readers will object that if you write your code correctly, you won't need to protect yourself from your own mistakes - "You can either buy leaky pens and wear a pocket protector, or just buy pens that don't leak, period." While I agree with this philosophy, it is important to remember that when you're writing code, you're not buying pens - you're manufacturing pens for other people to stick in their pockets. Using const helps in manufacturing quality pens that don't leak.
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; const char* const GetName() { return m_szName; }; private: char* m_szName; }; Person P("Fred Jones"); MyBuggyPrint(P.GetName()); // error! Can't convert const char* const to char*
This brings up an interesting point. If you wrote your code this way, you'd have to go back and rewrite your MyBuggyPrint function to take a const char* const (hopefully fixing it in the process). This is a pretty inefficient way to code, so remember that you should use const as you go - don't try to make everything const correct after the fact. As you're writing a function like MyBuggyPrint, you should think "Hmmm...do I need to modify what the pointer points to? No...do I need to point the pointer somewhere else? No...so I will use a const char* const argument." Once you start thinking like this, it's easy to do, and it will keep you honest; once you start using const correctness, you have to use it everywhere.
With this philosophy, we could further modify the above example by having the Person constructor take a const char* const, instead of a char*. We could also further modify the GetName member function. We can declare it as:
class Person { public: Person(char* szNewName) { // make a copy of the string m_szName = _strdup(szNewName); }; ~Person() { delete[] m_szName; }; const char* const GetName() const { return m_szName; }; private: char* m_szName; };
If we declare GetName() as a const member function, then the following code is legal:
void PrintPerson(const Person* const pThePerson) { cout << pThePerson->GetName() << endl; // OK } // a const-reference is simply an alias to a const variable void PrintPerson2(const Person& thePerson) { cout << thePerson.GetName() << endl; // OK }
void PrintPerson(const Person* const pThePerson) { // error - non-const member function called cout << pThePerson->GetName() << endl; } void PrintPerson2(const Person& thePerson) { // error - non-const member function called cout << thePerson.GetName() << endl; }
A const member function in class Person would take a const class Person* const (const pointer to const Person) as its implicit first argument, whereas a non-const member function in class Person would take a class Person* const (const pointer to changeable Person) as its first argument.
You could make a fake this pointer using explicit casting:
class MyData { public: /* the first time, do calculation, cache result in m_lCache, and set m_bCacheValid to TRUE. In subsequent calls, if m_bCacheValid is TRUE then return m_lCache instead of recalculating */ long ExpensiveCalculation() const { if (FALSE == m_bCacheValid) { MyData* fakeThis = const_cast<MyData*>this; fakeThis->m_bCacheValid = TRUE; fakeThis->m_lCache = ::SomeFormula(m_internalData); } return m_lCache; }; // change internal data and set m_bCacheValid to FALSE to force recalc next time void ChangeData() { m_bCacheValid = FALSE; m_internalData = ::SomethingElse(); }; private: data m_internalData; long m_lCache; bool m_bCacheValid; };
class MyData { public: /* the first time, do calculation, cache result in m_lCache, and set m_bCacheValid to TRUE. In subsequent calls, if m_bCacheValid is TRUE then return m_lCache instead of recalculating */ long ExpensiveCalculation() const { if (FALSE == m_bCacheValid) { m_bCacheValid = TRUE; m_lCache = ::SomeFormula(m_internalData); } return m_lCache; }; // change data and set m_bCacheValid to FALSE to force recalc next time void ChangeData() { }; private: data m_internalData; mutable long m_lCache; mutable bool m_bCacheValid; };