Member Class Objects & Initializer Lists

Member Class Objects & Initializer Lists

Diving into the next set of topics…

Member Class Objects

A class can contain members which are themselves instances of other classes — like a struct within a struct. 

These are member class objects.  For example:

//
//  Complex.h
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_h
#define Complex_h

// complex.h
class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  Complex(const Complex &src);
  
  void Print();
  // ...
private:
  double m_real, m_imag;
};

class ComplexLine
{
public:
  ComplexLine();
  ComplexLine(const Complex &start,
              const Complex &end);
  
  void Print();
private:
  Complex m_start, m_end;
};


#endif /* Complex_h */
//
//  Complex.cpp
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

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

Complex::Complex(double real, double imag)
{
  m_real = real;
  m_imag = imag;
}

Complex::Complex(const Complex &src)
{
  m_real = src.m_real;
  m_imag = src.m_imag;
}

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

// Class ComplexLine

ComplexLine::ComplexLine()
{
  m_start = 0.0; 
  m_end = 0.0;
}

ComplexLine::ComplexLine(
              const Complex &start,
              const Complex &end)
{
  m_start = start; 
  m_end = end;
}

void ComplexLine::Print()
{
  std::cout << "ComplexLine: [";
  m_start.Print();
  std::cout << ", ";
  m_end.Print();
  std::cout << "]" << std::endl;
}

Here’s a test program for this:

//
//  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();
  return 0;
}

Which produces the following output:

ComplexLine: [Complex: [0, 0], Complex: [0, 0]]
ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]
Program ended with exit code: 0

But suppose we remove the default constructor:

//
//  Complex.h
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_h
#define Complex_h

// complex.h
class Complex
{
public:
  // Complex(double real = 0.0, double imag = 0.0);
  Complex(const Complex &src);
  
  void Print();
  // ...
private:
  double m_real, m_imag;
};

class ComplexLine
{
public:
  ComplexLine();
  ComplexLine(const Complex &start,
              const Complex &end);
  
  void Print();
private:
  Complex m_start, m_end;
};

#endif /* Complex_h */

Now, we get compile-time errors in Complex.cpp

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

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

/*
 Complex::Complex(double real, double imag)
{
  m_real = real;
  m_imag = imag;
}
 */

Complex::Complex(const Complex &src)
{
  m_real = src.m_real;
  m_imag = src.m_imag;
}

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

// Class ComplexLine

ComplexLine::ComplexLine()
{
  m_start = 0.0; 
  m_end = 0.0;
}

ComplexLine::ComplexLine(
              const Complex &start,
              const Complex &end)
{
  m_start = start; 
  m_end = end;
}

void ComplexLine::Print()
{
  std::cout << "ComplexLine: [";
  m_start.Print();
  std::cout << ", ";
  m_end.Print();
  std::cout << "]" << std::endl;
}
Complex.cpp:32:14 Constructor for 'ComplexLine' must explicitly initialize the member 'm_start' which does not have a default constructor

Complex.cpp:32:14 Constructor for 'ComplexLine' must explicitly initialize the member 'm_end' which does not have a default constructor

Complex.cpp:34:11 No viable overloaded '='

Complex.cpp:35:9 No viable overloaded '='

Complex.cpp:38:14 Constructor for 'ComplexLine' must explicitly initialize the member 'm_start' which does not have a default constructor

Complex.cpp:38:14 Constructor for 'ComplexLine' must explicitly initialize the member 'm_end' which does not have a default constructor

The problem is that we wrote code to assign, not initialize the member class object.

The compiler automatically generated a call to the default constructor for the member class object to perform the initialization.

When we don’t have a default constructor for the member class object, the compiler doesn’t know how to initialize the object.

Constructor Initializer Lists

In order to initialize the member class object, we have to use a constructor initializer list:

class X
{
public:
	X(int x, int y) : <initializer-list>
	{
		// Body
	}
	// ...
};

Constructor initializer lists are only used with constructors.

An initializer list (preceded by a colon) follows the constructor’s argument list, and precedes the constructor’s body.

An initializer list contains one or more items, separated by commas.

An initializer list item looks somewhat like a function call.

Here’s the previous example, converted to using initializer lists:

The Complex.h file is unchanged:

//
//  Complex.h
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_h
#define Complex_h

// complex.h
class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  Complex(const Complex &src);
  
  void Print();
  // ...
private:
  double m_real, m_imag;
};

class ComplexLine
{
public:
  ComplexLine();
  ComplexLine(const Complex &start,
              const Complex &end);
  
  void Print();
private:
  Complex m_start, m_end;
};

#endif /* Complex_h */

The initializer lists are added to Complex.cpp:

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

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

Complex::Complex(double real, double imag)
  : m_real(real), m_imag(imag)
{ }


Complex::Complex(const Complex &src)
  : m_real(src.m_real), m_imag(src.m_imag)
{ }

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

// Class ComplexLine

ComplexLine::ComplexLine()
  : m_start(0.0), m_end(0.0)
{ }

ComplexLine::ComplexLine(
              const Complex &start,
              const Complex &end)
  : m_start(start), m_end(end)
{ }

void ComplexLine::Print()
{
  std::cout << "ComplexLine: [";
  m_start.Print();
  std::cout << ", ";
  m_end.Print();
  std::cout << "]" << std::endl;
}

The test program is unchanged:

//
//  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();
  return 0;
}

Which produces the same output as before:

ComplexLine: [Complex: [0, 0], Complex: [0, 0]]
ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]
Program ended with exit code: 0

Note that, in this case, this leaves the constructor bodies empty — which is not unusual!

Recommendation

While you can get away with using assignment in constructors, it is something to be discouraged!

Why?

  • Because it’s important to understand that you’re initializing, not assigning, and…
  • Because it’s more efficient to use initialization. 
    Remember that the compiler will try to initialize things for you, even if you don’t do it.

Efficiency: Assignment vs Initializer Lists

Let’s examine what happens when we use assignment instead of initialization. 

First, in the previous Complex/ComplexLine example, we’ll return to using assignments within the constructors, add some operator= functions to track when assignment happens, and we’ll instrument the classes with printouts. 

Here are the Complex.h and Complex.cpp files:

//
//  Complex.h
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_h
#define Complex_h

// complex.h
class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  Complex(const Complex &src);
  ~Complex();
  
  void Print();
  
  Complex &operator=(const Complex &rhs);
  Complex &operator=(double rhs);

  // ...
private:
  double m_real, m_imag;
};

class ComplexLine
{
public:
  ComplexLine();
  ComplexLine(const Complex &start,
              const Complex &end);
  ~ComplexLine();
  
  void Print();
  
  ComplexLine &operator=(const ComplexLine &rhs);

private:
  Complex m_start, m_end;
};

#endif /* Complex_h */
//
//  Complex.cpp
//  Member Class Objects
//
//  Created by Bryan Higgs on 8/22/24.
//

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

Complex::Complex(double real, double imag)
{
  std::cout << "Complex(double real, double imag)"
            << std::endl;
  m_real = real;
  m_imag = imag;
}

Complex::Complex(const Complex &src)
{
  std::cout << "Complex(const Complex &src)" << std::endl;
  m_real = src.m_real;
  m_imag = src.m_imag;
}

Complex::~Complex()
{
  std::cout << "~Complex()" << std::endl;
}

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

Complex &Complex::operator=(const Complex &rhs)
{
  std::cout << "Complex::operator=(const Complex &rhs)"
            << std::endl;
  if (this != &rhs)
  {
    m_real = rhs.m_real;
    m_imag = rhs.m_imag;
  }
  return *this;
}

Complex &Complex::operator=(double rhs)
{
  std::cout << "Complex::operator=(double rhs)"
            << std::endl;
  m_real = rhs;
  m_imag = 0.0;
  return *this;
}


// Class ComplexLine

ComplexLine::ComplexLine()
{
  std::cout << "ComplexLine()" << std::endl;
  m_start = 0.0;
  m_end = 0.0;
}

ComplexLine::ComplexLine(
              const Complex &start,
              const Complex &end)
{
  std::cout << "ComplexLine(const Complex & start, "
                            "const Complex &end)"
            << std::endl;
  m_start = start;
  m_end = end;
}

ComplexLine::~ComplexLine()
{
  std::cout << "~ComplexLine()" << std::endl;
}

void ComplexLine::Print()
{
  std::cout << "ComplexLine: [";
  m_start.Print();
  std::cout << ", ";
  m_end.Print();
  std::cout << "]" << std::endl;
}

ComplexLine &ComplexLine::operator=(const ComplexLine &rhs)
{
  std::cout << "ComplexLine::operator=(const Complex &rhs)"
            << std::endl;
  if (this != &rhs)
  {
    m_start = rhs.m_start;
    m_end = rhs.m_end;
  }
  return *this;
}

The test program is again unchanged:

//
//  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();
  return 0;
}

And it now produces the following output:

Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine()
Complex::operator=(double rhs)
Complex::operator=(double rhs)

ComplexLine: [Complex: [0, 0], Complex: [0, 0]]

Complex(double real, double imag)

Complex(double real, double imag)

Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine(const Complex & start, const Complex &end)
Complex::operator=(const Complex &rhs)
Complex::operator=(const Complex &rhs)
~Complex()
~Complex()

ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]

~ComplexLine()
~Complex()
~Complex()
~ComplexLine()
~Complex()
~Complex()
Program ended with exit code: 0

Go through that output and see if you can understand where each line comes from in the code.

Now, let’s change to initializer lists:

The only file to change is Complex.cpp:

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

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

Complex::Complex(double real, double imag)
  : m_real(real), m_imag(imag)
{
  std::cout << "Complex(double real, double imag)"
            << std::endl;
}

Complex::Complex(const Complex &src)
  : m_real(src.m_real), m_imag(src.m_imag)
{
  std::cout << "Complex(const Complex &src)" << std::endl;
}

Complex::~Complex()
{
  std::cout << "~Complex()" << std::endl;
}

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

Complex &Complex::operator=(const Complex &rhs)
{
  std::cout << "Complex::operator=(const Complex &rhs)"
            << std::endl;
  if (this != &rhs)
  {
    m_real = rhs.m_real;
    m_imag = rhs.m_imag;
  }
  return *this;
}

Complex &Complex::operator=(double rhs)
{
  std::cout << "Complex::operator=(double rhs)"
            << std::endl;
  m_real = rhs;
  m_imag = 0.0;
  return *this;
}


// Class ComplexLine

ComplexLine::ComplexLine()
  : m_start(0.0), m_end(0.0)
{
  std::cout << "ComplexLine()" << std::endl;
}

ComplexLine::ComplexLine(
              const Complex &start,
              const Complex &end)
  : m_start(start), m_end(end)
{
  std::cout << "ComplexLine(const Complex & start, "
                            "const Complex &end)"
            << std::endl;
}

ComplexLine::~ComplexLine()
{
  std::cout << "~ComplexLine()" << std::endl;
}

void ComplexLine::Print()
{
  std::cout << "ComplexLine: [";
  m_start.Print();
  std::cout << ", ";
  m_end.Print();
  std::cout << "]" << std::endl;
}

ComplexLine &ComplexLine::operator=(const ComplexLine &rhs)
{
  std::cout << "ComplexLine::operator=(const Complex &rhs)"
            << std::endl;
  if (this != &rhs)
  {
    m_start = rhs.m_start;
    m_end = rhs.m_end;
  }
  return *this;
}

… and here’s the new output:

Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine()
ComplexLine: [Complex: [0, 0], Complex: [0, 0]]
Complex(double real, double imag)
Complex(double real, double imag)
Complex(const Complex &src)
Complex(const Complex &src)
ComplexLine(const Complex & start, const Complex &end)
~Complex()
~Complex()
ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]
~ComplexLine()
~Complex()
~Complex()
~ComplexLine()
~Complex()
~Complex()
Program ended with exit code: 0

Compare the outputs side-by-side:

Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine()
Complex::operator=(double rhs)
Complex::operator=(double rhs)

ComplexLine: [Complex: [0, 0], Complex: [0, 0]]

Complex(double real, double imag)

Complex(double real, double imag)


Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine(const Complex & start, const Complex &end)
Complex::operator=(const Complex &rhs)
Complex::operator=(const Complex &rhs)
~Complex()
~Complex()

ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]

~ComplexLine()
~Complex()
~Complex()
~ComplexLine()
~Complex()
~Complex()
Program ended with exit code: 0
Complex(double real, double imag)
Complex(double real, double imag)
ComplexLine()



ComplexLine: [Complex: [0, 0], Complex: [0, 0]]

Complex(double real, double imag)

Complex(double real, double imag)
Complex(const Complex &src)
Complex(const Complex &src)


ComplexLine(const Complex & start, const Complex &end)


~Complex()
~Complex()

ComplexLine: [Complex: [2, 1.5], Complex: [3.9, 2.5]]

~ComplexLine()
~Complex()
~Complex()
~ComplexLine()
~Complex()
~Complex()
Program ended with exit code: 0

See if you can explain the differences.

Note that using initializer lists has removed the need for a number of operations!

…and this was a trivially simple class.  In non-trivial classes, you can produce significant savings!

When Constructor Intializer Lists are Required

There are cases where you don’t have a choice — where you must use initializer lists:

  • const data members
  • reference data members
  • member class objects with no default constructor

For example:

//
//  main.cpp
//  Required Constructor Initializer Lists
//
//  Created by Bryan Higgs on 8/23/24.
//

#include <iostream>

class NoDefaultConstructor
{
public:
  NoDefaultConstructor(double value)
    : m_value(value)
  { }
  // ...
private:
  NoDefaultConstructor(); // Disable
    
  double m_value;
};

class RequiresInitializerList
{
public:
  RequiresInitializerList(const int constValue,
                          int &refValue,
                          NoDefaultConstructor & ndc)
  {
    m_constValue = constValue;
    m_refValue = refValue;
    m_ndc = ndc;
  }
private:
  const int            m_constValue;
  int                 &m_refValue;
  NoDefaultConstructor m_ndc;
};

int main(int argc, const char * argv[]) 
{
  NoDefaultConstructor ndc(4.2);
  int value1 = 13;
  int value2 = 73;
    
  RequiresInitializerList(value1, value2, ndc);
  return 0;
}

This gives compile time errors:

main.cpp:26:3 Constructor for 'RequiresInitializerList' must explicitly initialize the const member 'm_constValue'

main.cpp:26:3 Constructor for 'RequiresInitializerList' must explicitly initialize the reference member 'm_refValue'

main.cpp:26:3 Field of type 'NoDefaultConstructor' has private default constructor

main.cpp:30:18 Cannot assign to non-static data member 'm_constValue' with const-qualified type 'const int'

If we change to using initializers,

//
//  main.cpp
//  Required Constructor Initializer Lists
//
//  Created by Bryan Higgs on 8/23/24.
//

#include <iostream>

class NoDefaultConstructor
{
public:
  NoDefaultConstructor(double value)
    : m_value(value)
  { }
  // ...
private:
  NoDefaultConstructor(); // Disable
    
  double m_value;
};

class RequiresInitializerList
{
public:
  RequiresInitializerList(const int constValue,
                          int &refValue,
                          NoDefaultConstructor & ndc)
    : m_constValue(constValue), m_refValue(refValue), m_ndc(ndc)
  { }
private:
  const int            m_constValue;
  int                 &m_refValue;
  NoDefaultConstructor m_ndc;
};

int main(int argc, const char * argv[]) 
{
  NoDefaultConstructor ndc(4.2);
  int value1 = 13;
  int value2 = 73;
    
  RequiresInitializerList(value1, value2, ndc);
  return 0;
}

the program runs correctly.

It is always better to use initializer lists to perform initialization of class data members.

If you don’t use initializer lists, you’re probably doing assignment in the constructor body.  This is very inefficient, because the compiler is doing implicit initialization (perhaps incorrectly) for you anyway.

Summary

Well, we’ve learned still more about classes:

  • Member Class Objects
  • Constructor Initializer Lists

As you can see, classes are the central feature of C++!

Index