const, Reference, and static Members

const, Reference, and static Members

const Members

C++ has:

  • const data members
  • const member functions

const Data Members

It should be clear that a const data member of a class:

  • must be initialized (possibly via a default constructor) in the containing class constructors
  • may not change during the lifetime of the class instance (that’s the meaning of const)

This can be useful;  it is too seldom used!

const Member Functions

So, what is a const member function?

  • It’s a member function that promises not to modify its class instance.
  • So, all member functions which are accessor functions should be declared const.
  • Also, member functions like Print() (etc.) should be declared const.

Here’s the syntax:

class Complex
{
	// ...
	void Print() const;
	double GetReal() const { return real; }
	double GetImag() const { return imag; }
	// ...
};

where the const is placed after the function argument list, and before the function body.

Example:

Let’s use our Complex class from the previous Complex/ComplexLine example:

//
//  main.cpp
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.h"

int main(int argc, const char * argv[]) 
{
  ComplexLine line1;
  line1.Print();
  
  ComplexLine line2(Complex(2.0, 1.5),
                    Complex(3.9, 2.5));
  line2.Print();
  
  const Complex c(3.4, 5.6);  	// OK
	c.Print();	
	
  return 0;
}

But it gives a compile time error:

main.cpp:21:3 'this' argument to member function 'Print' has type 'const Complex', but function is not marked const

Since the C++ compiler takes us seriously when we say const, it doesn’t know that the Print() member function won’t change the instance.

By adding const, we are telling the compiler that the member function will not change anything in the instance.

Note that the compiler will check the member function implementation to ensure that it does not change anything!

We fix the problem by simply adding ‘const’ to the declaration of member function Print:

  void Print() const;

… and also to its definition:

void Complex::Print() const
{
  std::cout << "Complex: [" << m_real << ", " << m_imag << "]";
}

The program now compiles and runs correctly.

The this pointer for a const member function of class T is declared thusly:

const T * const this;	// meaning?

which enforces the rules for code inside the const member function.

Breaking the Rules

There are times when you would like a member function to be:

‘physically const

  • It truly does not change its instance.
  • Typical, straightforward usage.
Logical const-ness

There are other times when you would like a member function to be:

‘logically const

  • It appears to the outside world not to change its instance
  • In fact, it does change something in the instance (an implementation detail?)
  • To do this, one must explicitly cast the this pointer:
double Real() const
{
    ((Complex *)this)->real_accesses++; // Count of accesses (instrumentation)
    return real;
}

Clearly, this is something to do only if circumstances warrant. It should not be normal usage — especially not to get your program to compile in order to get around a problem you don’t yet understand.

It is typically useful for keeping track of things like method invocation counts, and similar things.

mutable Data Members

The C++ standard has now blessed this as a supported practice. They have introduced the keyword mutable, that allows a member variable of a const object to be modified. It’s used to specify that a particular data member of a const object can be modified without affecting the object’s const-ness.

Here’s an example:

//
//  Toys.hpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#ifndef Toys_hpp
#define Toys_hpp

#include <string>

class Toy
{
public:
  Toy(std::string& name);
  
  std::string& getName() const;
  
  int getAccessCount() const;
    
private:
    std::string& m_name;
    mutable int m_accessCount = 0;
};

#endif /* Toys_hpp */
//
//  Toys.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include "Toys.hpp"

Toy::Toy(std::string& name)
  : m_name(name)
{}

std::string& Toy::getName() const
{
  m_accessCount++;
  return m_name;
}

int Toy::getAccessCount() const
{
  return m_accessCount;
}

Here’s a main program to run this code:

//
//  main.cpp
//  const, volatile, mutable
//
//  Created by Bryan Higgs on 2/10/25.
//

#include <iostream>
#include <array>
#include "Toys.hpp"

void listToys(const Toy &firstToy, const Toy &secondToy)
{
  std::cout << "Toy [1] : " << firstToy.getName() << std::endl;
  std::cout << "Toy [2] : " << secondToy.getName() << std::endl;
}

int main(int argc, const char * argv[]) 
{
  static std::string Train("Train");
  const Toy train(Train);

  static std::string Barbie("Barbie");
  const Toy doll(Barbie);
  
  listToys(train, doll);
  
  std::cout << "Number of accesses to: " << train.getName() << " = " << train.getAccessCount() << std::endl;
  std::cout << "Number of accesses to: " << doll.getName() << " = " << doll.getAccessCount() << std::endl;

  return 0;
}

Here is what the main program outputs:

Toy [1] : Train
Toy [2] : Barbie
Number of accesses to: Train = 2
Number of accesses to: Barbie = 2
Program ended with exit code: 0

Note the use of the mutable keyword in the Toys.hpp header file, and the use of const in the main program.

The mutable keyword is useful in several scenarios:

  1. Caching
  2. Lazy evaluation
  3. Thread synchronization
  4. Maintaining logical const-ness

static Members

A class may contain static members.

A static data member:

  • Has only a single instance per class (actually per class hierarchy)
  • Is shared by all instances of its class
  • Exists even if no instances of that class (yet) exist.
  • Must be initialized outside its class

A static member function:

  • Is not associated with any specific instance of its class (it is associated with the class, not an instance)
  • Can be called whether or not any instances of the class (yet) exist
  • Does not have any this pointer (it’s not associated with an instance)

static members are still subject to access control (private/public)

Example:

Here is a Person class that collects a population of people, and reports on its current state, giving the current population, and a census of the people in that population:

NOTE: This class uses a simple implementation of a linked list to keep track of the people in its current population. I do not recommend this approach for your code; C++ has, in its Standard Template Library (STL) many useful implementations of structures and algorithms, including a linked list, and many more. It would be far better to use an STL linked list, or similar.

The problem is that we haven’t yet learned how to use the STL, and that takes a while to master.

All in due time…

Note that this class contains three static data members (first, last, and count) to keep track of the population and census, and two static member functions that allow the reporting of the population and census.

Remember that a single instance of each of these static data members and static member functions exists, independent of how many instances of the Person class exist, or even if there are none.

//
//  Person.hpp
//  Static Members
//
//  Created by Bryan Higgs on 8/23/24.
//

#ifndef Person_hpp
#define Person_hpp

class Person
{
public:
  // static member functions
  static unsigned int Population() { return count; }
  static void Census();

  // Non-static member functions
  const char *Name() const     { return name; }

  // No default constructor
    
  // Constructor with one argument
  Person(const char *n);
    
  // Destructor
  ~Person();

private:
  // static data members
  static Person       *first;    // List header
  static Person       *last;    //  ...
    
  static unsigned int  count;    // Number in list

  // Non-static data members
  Person              *next;    // Item in list
  Person              *prev;    //  ...

  char                *name;    // Person's name
};

#endif /* Person_hpp */
//
//  Person.cpp
//  Static Members
//
//  Created by Bryan Higgs on 8/23/24.
//

#include <stdio.h>
#include <iostream>
#include "Person.hpp"

// Initialize static data members
Person *Person::first = 0;
Person *Person::last  = 0;
unsigned int Person::count = 0;

// Constructor
Person::Person(const char *n)
  : name(new char[strlen(n) + 1])
{
  // Insert this into the linked
  // list, at the end of the list.
  if (first == 0)   // first in list?
  {
    first = this;
    last  = this;
    prev  = 0;
  }
  else
  {
    last->next = this;
    prev = last;
    last = this;
  }
  next = 0;
  count++;    // one more in list
  strcpy(name, n);
}

// Destructor
Person::~Person()
{
  // Remove this from the linked list
  if (prev == 0)  // first in list?
     first = next;
  else
     prev->next = next;
  if (next == 0)  // last in list?
     last = prev;
  else
     next->prev = prev;
  --count;      // one fewer in list
  delete [] name;
}

// static member function
void Person::Census()
{
  std::cout << "Current census:" << std::endl;
  for (Person *p = first; p != 0; p = p->next)
  {
     std::cout << p->Name() << std::endl;
  }
}

Here’s a test program for trying this out:

//
//  main.cpp
//  Static Members
//
//  Created by Bryan Higgs on 8/23/24.
//

#include <iostream>
#include "Person.hpp"

// Regular function to report on the current population
void report()
{
  std::cout << "Current population: "
        << Person::Population() << std::endl;
  Person::Census();
}

void sub()
{
  Person bs("Bjarne Stroustrup");
  Person ed("Edsger Dijkstra");
  report();
}

int main(int argc, const char * argv[])
{
  std::cout << "report();" << std::endl;
  report();
  std::cout << "sub();" << std::endl;
  sub();
  std::cout << "report();" << std::endl;
  report();
  
  Person *bc = new Person("Brad Cox");
  Person *al = new Person("Ada Lovelace");
  std::cout << "report();" << std::endl;
  report();
  
  std::cout << "deleting Bjarne and Ada" << std::endl;
  delete bc;
  delete al;
  std::cout << "report();" << std::endl;
  report();
  
  return 0;
}

… and here is what it outputs:

report();
Current population: 0
Current census:
sub();
Current population: 2
Current census:
Bjarne Stroustrup
Edsger Dijkstra
report();
Current population: 0
Current census:
report();
Current population: 2
Current census:
Brad Cox
Ada Lovelace
deleting Bjarne and Ada
report();
Current population: 0
Current census:
Program ended with exit code: 0

When used properly:

  • static data members can replace global data
  • static member functions can replace global functions

All while retaining encapsulation and data abstraction.

Think about how you might be able to use static class members!

They can be extremely useful when used properly.

Index