<Effective C++> rule 03: Use const whenever possible

What can const decorate?

  • const can modify constants outside a global or namespace class.

  • const can also modify objects that are declared static in a file, function, or scope.

  • const can also modify static and non-static member variables within a class.

  • When a const is combined with a pointer, it can modify both the pointer itself and the object it refers to.

    • When const is to the left of *, the object pointed to by the pointer is a constant.
    • When const is to the right of *, it means that the pointer itself is a constant (that is, the pointer always points to this object).
    • When const is on both sides of *, both the pointer itself and the object to which the pointer is pointing are constants.

    For example:

    char greeting[] = "Hello";
    char* p = greeting;					// non-const pointer, non-const data (neither the pointer itself nor the object pointed to by the pointer is a constant)
    const char* p = greeting;			// non-const pointer, const data (pointer refers to a constant object, pointer is not a constant)
    char* const p = greeting;			// const pointer, non-const data (pointer is constant, pointer is not pointing to object)
    const char* const p = greeting;		 // const pointer, const data (both pointer itself and pointer object are constants)
    

    PS: Sometimes const is written not necessarily before the object type, but between the object type and *, which can also be used to indicate that the pointer is pointing to a constant object.

    For example:

    void f1(const Widget* pw);
    void f2(Widget const* pw);
    

    Both are equivalent, indicating that the pw points to a const Widget* object.

STL Iterator and Pointer

Iterators act like T* pointers!

One thing to note is that declaring an iterator as const is actually equivalent to a T* const pointer.

  • That is, the pointer itself is a constant, and the object that the pointer refers to can actually be changed!

If we want to indicate that the pointer is pointing to a constant object, we need to use const _iterator.

Example:

std::vector<int> vec;
...

// Equivalent to T* const
const std::vector<int>::iterator iter = vec.begin();		// That is, iter itself is a constant
*iter = 10;		// No problem, modify the object the iterator points to
++iter;			// Error: Iterator is a constant

// Equivalent to const T*
std::vector<int>::const_iterator cIter = vec.begin();		// That is, the object that iter refers to is a constant
*cIter = 10; 	// Error: The object referred to by iter is a constant
++cIter;		// No problem, modify the object the iterator points to

Use const as much as possible

For function return values, if you do not need to modify parameters or local variables, you should declare their return value as const

This is primarily for security purposes, to prevent programmers from incorrectly modifying function return values, parameters, or objects that should not be modified.

const member function

Q: Why const member function?

A: There are two reasons.

  1. This lets you know that the function can change the object content.
  2. This "manipulates const objects".

An important C++ feature

Two member functions can be overloaded if they are constants only!

That is, for the same kind of operation, you can have two sets of logic for const and non-const objects.

Look at an example of a class TextBlock used to represent a piece of text:

class TextBlock {
public:
    ...
    const char& operator[](std::size_t position) const {	// operator[] handling const object
        return text[position];
    }
    char& operator[](std::size_t position) {			   // operator [] handling non-const objects
        return text[position];
    }	
private:
    std::string text;
};

For operator[], this can be called:

TextBlock tb("Hello");
std::cout << tb[0];				// Call operator[] for non-const object version
const TextBlock ctb("World");
std::cout << ctb[0];			// Call operator[] for const object version

There is also a more common example of calling const object version functions/operators:

void print(const TextBlock& ctb) {		
    std::cout << ctb[0];		// Call operator[] of const object version, const TextBlock::operator[]
}

The main difference between the overloaded two versions of a function is that the function dealing with the version of the const object cannot modify the object (it is understandable, because the object is const):

std::cout << tb[0];				// No problem, read a non-const object
tb[0] = 'x';				    // No problem, modify a non-const object
std::cout << ctb[0];			// No problem, read a const object
ctb[0] = 'x';					// Something the matter! A const object cannot be modified

One more thing to note here is that the return value type of non-const operator [] is char&not char.

  • If the return value type is char, the statement tb[0] ='x'; Unable to compile.
    • Because char is a built-in type, if the return value of a function is a built-in type, it is illegal to modify the return value of a function!
  • Because we want to modify the object, we have to pass by reference. Otherwise, we can only modify a copy and not modify the object.

Bitwise constraints and logical constraints

bitwise constness: A member function can be declared const only if it does not modify any of the object's member variables (except, of course, static member variables).

That is, bitwise constraints do not modify any bit in the object (so it is called bitwise constraints).

This is actually the definition of const itself. That is, no non-static member variable within the modified object has been modified.

What's wrong with bitwise constness?

There is a problem with bitwise constraints.

Consider the following scenarios:

  • There is a pointer object in the class, and a member function of bitwise constraints does not modify the pointer object.
  • However, the object this pointer points to does not belong to this class.
  • Therefore, it is possible to modify the object this pointer refers to in this member function of bitwise constraints.
  • However, this is also perfectly legal (compilation passes), because in theory this pointer refers to objects that do not belong to this class (only this pointer belongs to this class), and this is only a modification of the object that the pointer refers to (without modifying the pointer itself), so it is perfectly ok.

Example:

class CTextBook {
public:
    ...
    char& operator[] (std::size_t position) const {		// bitwise const declaration
        return pText[position];
    }
 private:
    char* pText;
}

Note that the return value here is char &, which is a reference, so it is possible to modify the value of the object the pointer points to.

  • However, the pText pointer is not modified here, so the compiler does not think it is a problem to declare this member function as const.

And as you just said, this is where you can modify the value of the object the pointer is pointing to:

const CTextBook cctb("Hello");		// Declare a Constant Object
char* pc = &cctb[0];			   // Call const operator[] to get data with a pointer to cctb
*pc = 'J';						  // cctb now has something like "Jello".

Grammatically, the above code is fine. The pointer pc is not modified, only the object cctb that pc points to is modified, and cctb does not belong to CTextBook, so operator[] meets the requirements of a const member function~

Logically, however, there were some "unexpected modifications". So there is no logical constraints

logical constness

The difference between logical constraints and bitwise constraints is that logical constraints don't care whether or not (physically) the bits of class objects have been modified; its focus is on whether or not there is a logical "make modification" (whatever the modification is).

  • In other words, logical constraints allow (physically) modifications to class objects, but primarily guarantee that logical modifications do not result in erroneous modifications.

Considering the TextBook example just now, we might want to cache the length of the text and read it according to the length of the cache.
Then there is the following code:

class CTextBook {
public:
    ...
    std::size_t length() const;
private:
    char* pText;
    std::size_t textLength;			// Length of cached read text
    bool lengthIsValid;				// Determine if the current length is valid (the text can continue reading)
};

std::size_t CTextBook::length() const {
    if (!lengthIsValid) {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
}

Of course, this code will fail because you are trying to modify two member variables of class CTextBook, textLength and lengthIsValid, within a const member function, CTextBook::length().

The solution is to use the mutable keyword to release the bitwise constraints on non-static member variables:

class CTextBook {
public:
    ...
    std::size_t length() const;
private:
    char* pText;
    // Declarations: Even within a const member function, these two member variables may be modified
    mutable std::size_t textLength;			
    mutable bool lengthIsValid;				
};

std::size_t CTextBook::length() const {
    if (!lengthIsValid) {				// Modify mutable member variables within const member function, no problem
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
}

Here, the non-static object of the class object is modified (physically) to ensure that there is no logical error in the modification, which is logical constness.

Avoid duplication in const and non-const member functions

As mentioned earlier, "Two member functions can be overloaded if they are constants only!"

So for these two overloaded functions, there may be a lot of duplicate code in them:

class TextBook {
public:
    ...
    cosnt char& operator[] (std::size_t position) const {
        ...				// bounds checkint
        ...				// Access log access data
        ...				// verify data integrity
        return text[position];
    }
    char& operator[] (std::size_t position) {
        ...				// bounds checkint
        ...				// Access log access data
        ...				// verify data integrity
        return text[position];
    }
private:
    std::string text;
};

Obviously, the overload of different constness es functions here causes a lot of code duplication, which can cause a lot of problems -- for example:

  • Poor maintenance
  • Code Volume Expansion
  • Compile time growth

Solution: casting away constness

One sentence summary: Call a function that handles the version of a const object in a function that handles a version of a non-const object.

  • In this way, only one copy of the same code is written.

The example above can be changed to something like this:

class TextBook {
public:
    ...
    // Functions that handle const object versions are still what they were
    cosnt char& operator[] (std::size_t position) const {
        ...				// bounds checkint
        ...				// Access log access data
        ...				// verify data integrity
        return text[position];
    }
    // Function call const char & operatorp[] handling non-const object versions
    char& operator[] (std::size_t position) {
        /*
        	Const_here The purpose of cast is to call const op[]
   			Static_ The purpose of cast is to eliminate the constness of the op[] called
        */
        return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    }
private:
    std::string text;
};

This way, by const_cast and static_ Combined with cast, const member function is used to implement non-const member function, avoiding code duplication!

summary

  • Use const as much as possible. Const can help the compiler check for erroneous usages, such as data that should not be modified.
  • const can be applied to objects, function parameters, function return types, member functions in any scope.
  • Biwise constraints are used for syntax checks by the compiler, while logical constraints (mutable if necessary) should be used when writing programs.
  • To avoid code duplication, use const_when the const member function and the non-const member function are essentially equivalent implementations Cast binding static_const. This calls the const member function in a non-const member function.

Tags: C++

Posted by 01706 on Tue, 19 Apr 2022 01:42:26 +0930