9.11. const And Classes

Time: 00:07:41 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Please review the following as needed:

Data passed into or returned from a function by value creates a new copy of the data. When the copy is complete, a program can modify either version without affecting the other. So, pass and return by value form a one-way flow of data. However, the expense of copying the data increases with its size - if the data size is large, the time needed to copy it is proportionally large.

Alternatively, pass and return by pointer or by reference is independent of the data size - it takes the same amount of time to pass a large data structure as it does to pass a simple variable. Both mechanisms maintain a single copy of the data but give it two names - one name defined in the calling scope and one in the function's scope. Changes to the data made through one name are visible through both names. So, pointers and references implement a two-way flow of data.

Sometimes, solving a problem calls for the speed and efficiency of pointers or references while simultaneously providing the security of copying the data. The const keyword introduced in chapter 6 allows programmers to write code that is both fast and secure. This section extends const to member functions.

const Parameters

Unmodified, pass-by-pointer and by-reference create an INOUT data flow, but the const keyword restricts them to an IN only flow. Chapter 6 illustrated how to apply const to references and pointers. With a small extension of the syntax, we also make the object that calls or is bound to a member function (see Figure 1) "constant" or unchangeable. When we place the const keyword after the argument list, it applies to the "this" pointer, making the object to which it points (the object bound to the function call) constant.

Function CallsFunction PrototypesRepresentations
b.function1(f);
void function1(const Foo& a_f);
An abstract representation of how pass by reference works. A rectangle represents the object named f, passed as an argument to the function named function1. function1 has a formal parameter named a_f. While function1 runs, the compiler makes both f and a_f refer to the same object.
(a)
b.function2(&f);
b.function3(&f);
b.function4(&f);
void function2(const Foo* a_f);
void function3(Foo* const a_f);
void function4(const Foo* const a_f);
An abstract representation of how pass by pointer or address works. A rectangle represents the object named f, whose address the program passes as an argument to function2. function2 has a formal parameter named a_f that is a pointer. The program stores the address of f in a_f so that a_f points to f. This situation is represented graphically by an arrow pointing from a_f to f.
(b)
b.function5();
void function5() const;
An abstract representation of how an object is bound to a function with the "this" pointer. Object b calls and is bound to the member function named function5. Whenever a program calls a member function, it passes the address of the calling object - the object on the left-hand side of the dot or arrow operator - to 'this,' which is a local function variable. A rectangle represents the object named b, whose address the program passes as an implicit parameter to function5. This situation is represented graphically by an arrow pointing from 'this' to b.
(c)
const parameters. The location of the const keyword determines to which parameter it applies. The above code fragments assume that Foo is the name of a class, that f is a Foo object, and that b is another object (an instance of Foo or some other class) that defines the called functions. Including the "const" keyword as a part of the parameter definition makes the parameter constant: any attempt to change it in the function will result in a compile-time error. The "const" keyword applies to individual parameters, so it must be repeated for each parameter held constant in the function.
  1. Pass by reference. The function cannot change the object referred to by a_f, which is the object named f in the scope where function1 is called.
  2. Pass by pointer.
    • function2: the function cannot change the object to which a_f points (the contents of the large rectangle cannot be changed). When used with pointers, this example represents the most common usage.
    • function3: the function cannot change the pointer (i.e., the address stored in a_f or the small square).
    • function4: the function cannot change either the object (the large rectangle) or the pointer (the small square).
  3. The this pointer. The implicit or "this" parameter is always passed by pointer. If it must not change, then the "const" keyword is placed at the end of the parameter list, outside and following the last parenthesis. In the function definition, the opening brace of the function body, follows const. Any attempt to change this in the function will also result in a compile-time error.

Getter Functions

To maintain strong encapsulation, programmers typically make member variables private. However, an application that uses a class may sometimes need to access a member variable. Access functions, getters and setters, are the most common solution. Setter functions can validate the data - ensuring that it is within bounds or formatted correctly - before storing it in an object. Getter functions allow an application program to "see" the value stored in a member variable. A well-formed getter operates as an OUT-only function, preventing the application from changing and possibly corrupting the object.

Like any function, getters generally perform a return-by-value, that is a return-by-copy. So, any change the application makes to the returned data only affects the copy and leaves the original data - still safely encapsulated in the object - untouched. The next figure illustrates this common behavior.

class Person
{
    private:
        string	name;
        int	height;
    public:
        Person(string n, int h)
            : name(n), height(h) {}

        string get_name() { return name; }
        int get_height() { return height; }
};
int main()
{
    Person p("Alice", 65);
    string local_name = p.get_name();

    cout << local_name << endl;
    local_name = "Carol";	// doesn't change p
    cout << p.get_name() << endl;
    cout << local_name << endl;

    return 0;
}
(a)(b)
Return-by-value getter functions. Getter functions that return-by-value allow an application program to access an object's member variables but prevent it from changing them.
  1. A class with member variables and member functions. Both getter functions return a copy of the data stored in the member variables.
  2. Changing the data that a getter returns only changes the copy, leaving the object unchanged. The program output is:
    Alice
    Alice
    Carol

But what happens if we change the name from a string to a C-string? Recall that arrays, and therefore C-strings, are always passed to and returned from functions by-pointer. Pointers implement a two-way data flow that can potentially weaken encapsulation by allowing an application to change the data saved in an object. The next example replaces string name; with char name[100] and, correspondingly, the return type of get_name() to char*.

class Person
{
    private:
        char	name[100];
        int	height;
    public:
        Person(char* n, int h)
            : height(h) { strcpy_s(name, 100, n); }

        char* get_name() { return name; }
        int get_height() { return height; }
};
int main()
{
    Person p("Alice", 65);
    char* local_name = p.get_name();

    cout << local_name << endl;
    strcpy_s(local_name, 100, "Carol");	// changes p
    cout << p.get_name() << endl;
    cout << local_name << endl;

    return 0;
}
(a)(b)
The problem of a return-by-pointer getter function. When used with functions, pointers implement a two-way flow of data. Moving data in two directions is sometimes useful, but in this case, it breaches the object's encapsulation by allowing the application to change a member variable directly.
  1. The name member is implemented as an array. Recall that arrays are always passed and returned by-pointers, which is reflected by the return type of the get_name getter function.
  2. Using the pointer that get_name returns allows the application program to change the data stored in the object. The program output is:
    Alice
    Carol
    Carol

We can restore the strong encapsulation demonstrated by Figure 2 even while continuing to store the person's name as a C-string by making the pointer returned by get_name() a constant.

class Person
{
    private:
        char name[100];
        int height;
    public:
        Person(char* n, int h)
            : height(h) { strcpy_s(name, 100, n); }

        const char* get_name() { return name; }
        int get_height() { return height; }
};
int main()
{
    Person p("Alice", 65);
    const char* local_name = p.get_name();

    cout << local_name << endl;
    strcpy_s(local_name, 100, "Carol");	// compile-time error
    cout << p.get_name() << endl;
    cout << local_name << endl;

    return 0;
}
(a)(b)
Returning a constant pointer. The effect of making a function's returned value constant ripples throughout an application program, one change leading to the next, with the result maintaining an object's encapsulation.
  1. The first step is adding the const keyword to the function's header. As illustrated previously, where the programmer places the "const" keyword impacts what feature is made constant. Placing const in this location makes the return value constant.
  2. Once the function's return value is made constant, any variables used to store the returned value must also be made constant. Failing to add const to the variable definition results in a compile-time error. Any attempt to change a constant variable (demonstrated with a call to the strcpy_s function in this example) also results in a compile-time error.