A place where programmers can discuss various programming topics and experiences.



Discussion Topic #1: Logical vs. Bitwise constness

One of my favorite C++ programming tips is the use of const as a semantic constraint. I use it whenever and wherever I can because it can significantly reduce logic errors and turn them into compile errors (a good thing). In order to use it properly, you need to know how C++ defines constness. There are two types of object constness in the C++ world and they affect class design in different ways.

C++ compilers, by default, support the use of bitwise constness. This means that no part of an object can be modified after instantiation when declared with the const modifier. Inside a class definition, this is great for "getter" functions that merely return values to the caller. By declaring the functions const, you are telling the caller (and the compiler) that this member function cannot modify any members of the class it is working on. Here's an example:

class MyObject
{
public:
    MyObject();  // constructor
    ~MyObject(); // destructor

    long GetObjectID() const;
    void SetObjectID(long lngID);

    long GetObjectLength() const;
    void SetObjectLength(long lngLength);

private:
    long m_lngID;
    long m_lngLength;
};

In the above example, you will notice that there are two class functions that are defined with the const modifier. This tells the compiler that the function will not modify either the m_lngID or m_lngLength class members. So, by default, the definitions of the two "get" functions above cannot contain code that changes those two private class members. Here's a sample function definition:

long MyObject::GetObjectID() const
{
    m_lngID = 25;       // ERROR!
    m_lngLength = 0;    // ERROR!
    return m_lngID;     // Allowed!
}

Above you will note that attempts to assign values to m_lngID and m_lngLength will cause the compiler to generate an error. Alternatively, the return statement will not generate an error b/c it is merely returning a copy of one of the class' data members. Its not changing it.

Now sometimes you need a const function that guarantees it won't change certain class members, but might change others. This brings about the idea of logical constness. Let's say you have a class member who's value can be changed (and that not affect your object's definition of constness). In C++ you can declare this member with the keyword mutable. This can be useful at times. Scott Meyers suggests that when designing interfaces, you should use logical constness. He makes a good point b/c mutable gives the designer the ability to create his/her own definition of what constness means for a particular object. Example below:

class MyObject
{
public:
    MyObject();  // constructor
    ~MyObject(); // destructor

    long GetObjectID() const;
    void SetObjectID(long lngID);

    long GetObjectLength() const;
    void SetObjectLength(long lngLength);

private:
    mutable long m_lngID;
    long m_lngLength;
};

The above class mirrors our earlier example in all aspects except that the m_lngID member is declared with the mutable modifier. This tells the compiler to allow modifications to this member inside of const class functions. Here's a sample function definition:

long MyObject::GetObjectID() const
{
    m_lngID = 25;    // Allowed b/c the member mutable
    m_lngLength = 0; // ERROR!
    return m_lngID;  // Allowed!
}

The above function definition shows how the compiler will treat both member variables based on whether they where declared using mutable.

So, from the examples above you can see how using const as a semantic constraint can be useful. The examples above also show how mutable allows the designer to define what constness means for a particular object. There is one final note to this topic and that is const modifiers aren't "full-proof" in their protection of class member modification. Let's add a new routine to our MyObject class that returns a pointer to a character string:

class MyObject
{
public:
    // Constructor, destructor, blah blah...

    char * GetObjectName() const;

private:
    char * m_strObjectName;
};

// Defintion
char * MyObject::GetObjectName() const
{
    return m_strObjectName;
}

The GetObjectName() function is declared as a const function, but the private m_strObjectName variable isn't safe from modification! How can that be you say? The reason is because the function is returning a pointer to the class' data member. Anyone can now take that pointer and do with it what they wish. Here's the correct way to declare the above routine and ensure the name variable isn't changed:

const char * GetObjectName() const;

This states that we aren't going to change m_strObjectName in the function and also that we are returning a pointer to a constant character string. The string that the pointer "points" to cannot be changed. This now guarantees that the caller can only query the value of the class' name member.

I hope this wasn't too confusing. If so, let me know and I'll attempt to further demonstrates the idea. The bottom line is that using const forces code semantics that improve code stability. Understanding how C++ defines constness also allows the designer to provide more customized object definitions.

- Gilemonster

Labels:

posted by Gilemonster @ 11:44 AM,

1 Comments:

At 5:00 AM, Blogger ittays said...

A nice article: very clear writing.

 

Post a Comment

Links to this post:

Create a Link

<< Home