about
10/20/2022
C++ Story Classes
C++ Story Code C++ Story Docs

Chapter #5 - C++ Classes

classes and value types

5.0 Prologue

Classes and class relationships are the building blocks for object-oriented design. The results are a collection of objects - instances of classes - that cooperate to conduct operations required of their program. We structure the designs using: inheritance, composition, aggregation, using, and friend class relationships.
Quick Starter Example - Simple Logger
This example creates and demonstrates a relatively simple logger, e.g., a class whose instances write messages into a stream. It's here to illustrate techniques for upcoming discussions:
  1. Use of the "= delete" suffix, discussed later in this chapter.
  2. Use of std::ostream pointer data member, but casting that to a derived std::fstream pointer with dynamic_cast in the open method and destructor. Doing this allows us to log to std::cout, a std::ostream instance, but also log to derived std::fstreams. We discuss this in chapter 5, and will refer back to this example at that time.
We've used some file stream operations in this demo. If you aren't comfortable with those, you can skip over them - those we discuss in detail in chapter 8. Code: Relatively Simple Logger class Logger { public: Logger() : pStream_(nullptr) {} Logger(std::ostream* pStr) : pStream_(pStr) {} Logger(const Logger& logger) = delete; ~Logger() { std::ofstream* pFStrm = dynamic_cast(pStream_); if (pFStrm != nullptr) pFStrm->close(); } Logger& operator=(const Logger& logger) = delete; void open(const std::string& fileName) { std::ofstream* pFStrm = dynamic_cast(pStream_); if (pFStrm != nullptr) pFStrm->open(fileName); } void write(const std::string& msg) { (*pStream_) << prefix_ << msg; } private: std::string prefix_ = "\n "; std::ostream* pStream_; }; std::unique_ptr makeOutFileStream( const std::string& fileName ) { std::unique_ptr pStrm(new std::ofstream(fileName)); return pStrm; } std::unique_ptr makeInFileStream( const std::string& fileName ) { std::unique_ptr pStrm(new std::ifstream(fileName)); return pStrm; } Using Code int main() { displayDemo("-- simple logger --"); std::string fileName = "log.txt"; /*----------------------------------------- Create anonymous scope for logging so logger destructor will be called before trying to open log file. */ { auto pOStrm = makeOutFileStream(fileName); if (!pOStrm->good()) { std::cout << "\n couldn't open \"" << fileName << " for writing\n\n"; return 1; } Logger logger(pOStrm.get()); logger.write("first log item"); logger.write("second log item"); logger.write("last log item"); } auto pIStrm = makeInFileStream(fileName); if (!pIStrm->good()) { std::cout << "\n couldn't open \"" << fileName << " for reading\n\n"; return 1; } std::cout << "\n " << pIStrm->rdbuf(); putline(2); } Output -- simple logger -- first log item second log item last log item We've seen a significantly more ambitious logger at the end of Chapter 3, in our discussion of testing. This example lays out, in a simpler way, the basic mechanisms needed to build a logger. Conclusion: C++ classes bind together data members and methods to provide a useful facility. They should have a single focus and be as simple as it is practical to make them. That's true for this example. To keep the class simple, we factored out file opening functions makeOutFileStream and makeInFileStream. We don't need them if we just want to log to the console.
This example presented one of three different logger classes discussed in this story. This one is intended to illustrate a more-than simple, but relatively small, useful class. We will do the same thing again in Chapter 6 to illustrate template class specialization. In the Epilogue of chapter 6, we present a fairly complete implementation of a useful logging facility. I intend to merge the best features of these three examples and put that in Logger repository.

5.1 Basic Classes

Fig 1. Class X Layout
A class is like a "cookie cutter". It stamps out a section of memory in the stackframe of its local scope, and initializes that memory with data required to create a valid object. Each time it's used to declare an instance, another piece of memory is allocated and initialized. Each class has a set of methods - functions associated with that specific class - providing operations on its allocated data when invoked. Each has code that is stored in static memory, and potentially many instances holding data usually stored in the stackframe of the function where it is declared. When an instance method is invoked, x.fun(param); xεX, the address of x is sent to the code for class X to use if fun modifies x's data. That address is identified by the reserved word "this". You may occasionally see references to this in methods of the class. Most use is implicit, but occasionally it must be used explicitly, as in assignment operators that return *this.

5.1.1 Class Methods

C++ classes define special methods: constructors, assignment operators, and destructors. Constructor method names are all the name of the class. Assignment operators use the operator= name and destructor names are the class name prepended with a ~ character.

Table 1. - Special Class Methods for Class X

Method SyntaxDescription
X() Default constructor builds instance of X with default state values.
X(const X& x) Copy constructor builds instance of X with copy of the state of an existing instance.
X(X&& x) Move constructor builds an instance and transfers ownership of a source object's state.
X& operator=(const X& x) Copy assignment copies the state of a source instance to the state of an existing destination instance.
X& operator=(X&& x) Move assignment transfers ownership of a source instance's state to the destination instance. Source object becomes invalid.
~X() Destructor returns class resources when instance goes out of scope.
Classes almost always provide additional custom methods appropriate for the class's role. Each of the special methods in Table 1. will be used and explained in the examples that follow.

5.1.2 Point Class Examples

We will explore here three example variations of a "simple" Point class, considering class data structure and its consequences on flexibility, complexity, and performance. Point is an abstraction of a physical point in some space with coordinates and name. There are choices for how we represent Point's state and the results of those choices are significant. In Example1::Point we store coordinates in a composed buffer and name in a composed string, as shown in Figure 1, below. This turns out to be rather inflexible, as explained below, so we design Example2::Point to compose a std::vector<double> to hold coordinate information and a string for its name. The third design, Example3::Point, uses an array of doubles, for performance, created on the heap this time, to configure the number of coordinates held by a Point.
Fig 1. Example1 Class Layout Fig 2. Example2 Class Layout Fig 3. Example3 Class Layout

5.1.3 Point Examples Code

In the example below, we implement the first point class. That might be used in a drawing program to define lines, rectangles, and other polygonal figures. This Point example stores its x, y, z coordinate information in an array of doubles composed by the class, along with a std::string to hold the Point's name.
Example1: Point First Class Example - Point: namespace Example1 { class Point { public: Point(const std::string& name = "none") : name_(name), coordinates_{ 0.0, 0.0, 0.0 } { } std::string& name() { return name_; } double& operator[](size_t i) { if (i < 0 || 3 <= i) throw std::exception("invalid index"); return coordinates_[i]; } size_t size() const { return 3; } private: std::string name_; double coordinates_[3]; }; void show(Point p) { std::cout << "\n " << p.name() << "\n "; for (size_t i = 0; i < p.size(); ++i) { std::cout << p[i] << " "; } std::cout << std::endl; } } Using code: displayDemo("--- Example1::Point ---"); Example1::Point p1("p1"); p1[0] = 1.0; p1[1] = 2.0; p1[2] = 3.0; show(p1); displayDemo("--- copy constr of p2 using src p1 ---"); Example1::Point p2{ p1 }; p2.name() = "p2"; show(p2); displayDemo("--- copy assignment of p3 from src p2"); Example1::Point p3("p3"); p3 = p2; p3.name() = "p3"; show(p3); Output: --- Example1::Point --- p1 1 2 3 --- copy construction of p2 using src p1 --- p2 1 2 3 --- copy assignment of p3 from src p2 p3 1 2 3
This class doesn't need to provide means to support copying and assignment. The compiler will generate methods for that, doing member-wise copy and assignment. The name_ member has correct copy semantics, providing those as part of its class design. The coordinate array requires a memory copy operation, but that is provided by compiler generated copy construction and copy assignment operations. Look at sections 3.2 and 3.3 to see an example for a struct. While this design works, and correctly supports locating points in three physical dimensions, there are two things I don't like about this code:
  1. Class methods are defined in-line in the body of the class declaration, Java style. That makes reading the interface harder than necessary. It's better C++ style to move method definitions out of the class declaration.
  2. This is an unnecessarily inflexible design. Defining the class's coordinate buffer as a composed member means that we must define the array size at compile time, because the array will be placed in static memory and the compiler does that.
If we wanted to use instances of this class in an air-traffic control system, we probably would want to store the time of observation as part of the point's coordinates, e.g., location in both time and space, so there are now four coordinates. An effective way to support configuring a class for needs of a specific application is to define its storage at run-time, based on some parameter passed to the class. We can do that by creating the buffer in the native heap, providing the array size as needed. One of the primary reasons for using the native heap is to allow allocating memory with size determined by the application, at run-time. In the revision, shown below, we will use std::vector<double> that does that buffer management internally for us.
Example2: Revised Point Revised Class Example - Point: namespace Example2 { class Point { public: Point(const std::string& name = "none", size_t N = 3); std::string& name(); double& operator[](size_t i); double operator[](size_t i) const; size_t size() const; private: std::string name_; std::vector<double> coordinates_; }; /*--- constructor ---*/ Point::Point(const std::string& name, size_t N) : name_(name), coordinates_(N) { } /*--- name accessor ---*/ std::string& Point::name() { return name_; } /*--- indexer for non-const instances ---*/ double& Point::operator[](size_t i) { if (i < 0 || coordinates_.size() <= i) throw std::exception("index out of range"); return coordinates_[i]; } /*--- indexer for const instances ---*/ double Point::operator[](size_t i) const { if (i < 0 || coordinates_.size() <= i) throw std::exception("index out of range"); return coordinates_[i]; } /*--- buffer size accessor ---*/ size_t Point::size() const { return coordinates_.size(); } } /*--- generic display function ---*/ template<typename T> void show(T t) { std::cout << "\n " << t.name() << "\n "; for (size_t i = 0; i < t.size(); ++i) { std::cout << t[i] << " "; } std::cout << std::endl; } Using code: displayDemo("--- Example2::Point ---"); Example2::Point p4("p4", 3); p4[0] = -1.0; p4[1] = 0.0; p4[2] = 1.0; show(p4); displayDemo("--- copy constr of p5 using src p4 ---"); Example2::Point p5{ p4 }; p5.name() = "p5"; show(p5); displayDemo("--- copy assignment of p6 from src p5"); Example2::Point p6("p6"); p6 = p5; p6.name() = "p6"; show(p6); Output: --- Example2::Point --- p4 -1 0 1 --- copy construction of p5 using src p4 --- p5 -1 0 1 --- copy assignment of p6 from src p5 p6 -1 0 1
The implementation of Example2::Point is better:
  1. Method definitions are factored out of Example2::Point class declaration which makes the class interface easier to comprehend. We've also provided a prologue comment for each method.
  2. We've added an index operator for constant points, as without that, using code cannot index a const point.
  3. Like Example1::Point, this implementation doesn't need a copy constructor, copy assignment operator, or destructor since the compiler generated methods work correctly. The reason for that is that all the data members have correct copy, assignment, and destruction semantics - all of the STL containers, like std::string and std::vector, have those properties.
This isn't the end of the Point story though; some applications may need very small, and fast components. Suppose that we want to implement an air quality monitor, for things like temperature, humidity, carbon monoxide, radon emissions, and volatile organic compounds. This application will probably run on a controller chip or Arduno, with limited storage. It will need to handle storage of an open-ended number of measurements, each one a coordinate in the air quality space, and do that relatively quickly to keep up with a specified logging cycle. For this, we've decided to go back to using a buffer as it will be slightly smaller and substantially faster than a std::vector<double>. To provide the flexability to handle an open-ended number of measurements, we will store the coordinate buffer in heap memory, allowing us to specify buffer size at run-time. This assumes that the instrument's operating system supports heap memory.
Example3: Point Revised Again Revised again Example: namespace Example3 { class Point { public: Point(const std::string& name = "none", size_t N = 3); Point(const Point& x); ~Point(); Point& operator=(const Point& x); std::string& name(); double& operator[](size_t i); double operator[](size_t i) const; size_t size() const; private: std::string name_; double* pBuffer_ = nullptr; size_t bufSize_ = 0; }; /*--- constructor ---*/ Point::Point(const std::string& name, size_t N) : name_(name), bufSize_(N), pBuffer_(new double[N]) { } /*--- copy constructor ---*/ Point::Point(const Point& x) : bufSize_(x.bufSize_), pBuffer_(new double[x.bufSize_]) { name_ = x.name_; memcpy(pBuffer_, x.pBuffer_, bufSize_ * sizeof(double)); } /*--- destructor ---*/ Point::~Point() { delete[] pBuffer_; } /*--- copy assignment operator ---*/ Point& Point::operator=(const Point& x) { if (this != &x) { /* won't assign name */ bufSize_ = x.bufSize_; delete pBuffer_; pBuffer_ = new double[bufSize_]; memcpy(pBuffer_, x.pBuffer_, bufSize_ * sizeof(double)); } return *this; } /*--- name accessor ---*/ std::string& Point::name() { return name_; } /*--- indexer for non-const instances ---*/ double& Point::operator[](size_t i) { if (i < 0 || bufSize_ <= i) throw std::exception("index out of range"); return *(pBuffer_ + i); } /*--- indexer for const instances ---*/ double Point::operator[](size_t i) const { if (i < 0 || bufSize_ <= i) throw std::exception("index out of range"); return *(pBuffer_ + i); } /*--- buffer size accessor ---*/ size_t Point::size() const { return bufSize_; } } Using code: displayDemo("--- Example3::Point ---"); Example3::Point p7("p7", 3); p7[0] = -0.5; p7[1] = 0.0; p7[2] = 0.5; show(p7); displayDemo("--- copy constr p8 using src p7 ---"); Example3::Point p8{ p7 }; p8.name() = "p8"; show(p8); displayDemo("--- copy assign p9 from src p8 ---"); Example3::Point p9("p9"); p9 = p8; show(p8); Output: --- Example3::Point --- p7 -0.5 0 0.5 --- copy construction of p8 using src p7 --- p8 -0.5 0 0.5 --- copy assignment of p9 from src p8 p8 -0.5 0 0.5
There are some things to note about this design:
  1. Now, instead of composing the data buffer, as we did in Example1::Point, we aggregate it, e.g., we point to a heap-based buffer with the pointer pBuffer_.
  2. We are now obliged to provide a copy constructior, copy assignment operator, and destructor. The presence of the pointer, pBuffer_, means that all the class data members no longer have correct copy, assignment, and destruction semantics. The class's generated methods would simply copy and assign pBuffer_, not what it points to. That would result in incorrect operation, as you can verify by commenting out those operations and running code, in the Chapter4-classes folder.
  3. The example shows you how to implement copy, assignment, and destruction operations for this common scenario.
  4. The Point design is now more complex that either of the first two, since we now must implement copies, assignment, and destruction. We would probably do some performance testing to be sure the improvement over Example2::Point was significant enough to warrant this choice.
If some of the implementation seems hard to understand, pause and look at STRCode.html presentation. It describes all the details of each of these methods, using a demonstration string class. We will also look again at all these methods in section 4.4 Class Anatomy.

5.2 Value Types

All three of the example Point implementations, above, have value type semantics.
Value Types: Value types can be copied and assigned. When a copy or assignment operation completes there are two instances of the type which, immediately following the operation, have the same state values, but are independent. Should using code change the state of one, the other's state is not affected. Note that this is not true for user-defined types built from classes in managed languages like C# and Java. The C++ programming language was designed, from the ground up, to support value semantics. It does that by:
  1. Providing compiler generated copy constructors, copy assignment operators, and destructors, for cases where all the class's base classes and data members have correct copy, assignment, and destruction semantics. For those cases, e.g., data members are fundamental types and/or STL containers, you should not implement those methods - the compiler will implement them correctly.
  2. When data members do not have correct copy, assignment, and destruction behaviours, the language supports definition of those operations as class methods, as we have done in Example3::Point. In these cases, e.g., when the class holds a pointer member to data in the native heap, you either implement those methods or disallow them, preventing the compiler from generating them.
  3. The compiler will not generate constructors or assignment operators if you declare them and use a = delete suffix.
C++ value types can be used in inheritance hierarchies. There is no other modern language that supports polymorphic value types. This is not a "show stopper". There are other ways to create designs that don't require polymorphic value types, but it is nice to have that design option.
Make sure you look at the reference below. It walks you through the implementation of all of the class methods required to implement value types:
Because of its support for value types, C++ is very effective for scientific and financial computing. Libraries supporting those domains will need to be able to make copies and assign instances of library classes. Matrix and vector operations are typical examples.

5.3 Scope-based Resource Allocation

The C++ language provides a very elegant mechanism, also known as "Resource Allocation Is Initialization" (RAII), for managing class resources based on local scope.
Scoped-based Resource Management Instances of classes allocate resources they need in constructors and, occasionally, in other member functions. All resources are deallocated in the class destructor. The C++ language guarantees that the destructor will be called when an instance goes out of scope, e.g., when the thread of program execution leaves the scope in which the instance was declared. This includes abnormal exit when an exception is thrown.
This mechanism makes handling pointer-based operations safe and simple. We wrap all heap operations in classes and handle pointer and heap interactions in class constructors and destructor. That avoids memory leaks and invalid memory access. Only one person has to get that right - the class designer - and since that processing happens only in the wrapping class it is easy to do that correctly. For those rare cases when we elect to access the heap outside confines of a class, C++ provides std::unique_ptr<T> and std::shared_ptr<T> that automatically delete heap allocations when they go out of scope, or the last reference becomes invalid, respectively. Users of a class can be blissfully ignorant of resource management details. They just create and use instances of the class and don't have to participate in management of resources it needs to function. Compare that to the resource management process for managed languages like Java and C#. Managed languages return allocated resources with garbage collection. That usually works well, but is non-deterministic. To ensure that shared resources like database readers and file streams are deallocated in a timely fashion every user has to know which classes are disposable and remember to call dispose on instances when their processing is done.

5.4 Class Anatomy Example

Class Anatomy is a demonstration of the most common and important parts of a C++ class. Its only purpose is to demonstrate syntax for the common operations and to illustrate when they are invoked. Each of these important methods announces to the console when it has been invoked, so you can correlate the syntax of invocation with the method syntax. We will see, for example, that the statement below:
    X x2 = x1;
is not an assignment. Only the X copy constructor is invoked. Because that can be confusing to those new to C++, I prefer to use the equivalent syntax:
    X x2{ x1 };
When you expand the Class Anatomy Code details dropdown, below, you will see that Anatomy output shows you what occurred for each method invocation.
Class Anatomy Code Anatomy Class class Anatomy { public: Anatomy(); // default Anatomy(const std::string& nameStr); // promo Anatomy(const Anatomy& a); // copy Anatomy(Anatomy&& a) noexcept; // move Anatomy& operator=(const Anatomy& a); // copy Anatomy& operator=(Anatomy&& a) noexcept; // move ~Anatomy(); // dtor void name(const std::string& nameStr); std::string name() const; size_t objNumber() const; template<typename T> void setValue(const T& t); template<typename T> T getValue() const; void showMsg( size_t n, const std::string& msg ); private: std::string name_; static size_t count; size_t myCount = 0; std::any value; constexpr static size_t MaxMsg = 6; std::string invMsg[MaxMsg]{ "default ctor invoked", "copy ctor invoked", "move ctor invoked", "copy assignment invoked", "move assignment invoked", "dtor invoked" }; }; size_t Anatomy::count = 0; void Anatomy::showMsg( size_t n, const std::string& msg ) { if (Assert(0 <= n && n < MaxMsg)) return; std::string localMsg = invMsg[n]; std::cout << "\n " << localMsg; if (msg.size() > 0) std::cout << ", " << msg; } /*--- default constructor ---*/ Anatomy::Anatomy() : value(std::any()), name_("unknown") { myCount = ++count; showMsg(0, "obj #" + std::to_string(myCount)); } /*--- promotion constructor ---*/ Anatomy::Anatomy(const std::string& nameStr) : value(std::any()), name_(nameStr) { myCount = ++count; showMsg(0, "obj #" + std::to_string(myCount)); } /*--- copy constructor ---*/ Anatomy::Anatomy(const Anatomy& a) : value(a.value), name_(a.name_) { myCount = ++count; showMsg(1, "obj #" + std::to_string(myCount)); } /*--- move constructor ---*/ Anatomy::Anatomy(Anatomy&& a) noexcept : value(std::move(a.value)), name_(std::move(a.name_)) { myCount = ++count; showMsg(2, "obj #" + std::to_string(myCount)); } /*--- copy assignment operator ---*/ Anatomy& Anatomy::operator=(const Anatomy& a) { if (this != &a) { // don't want to rename, so no copy value = a.value; } showMsg(3, "obj #" + std::to_string(myCount)); return *this; } /*--- move assignment operator ---*/ Anatomy& Anatomy::operator=(Anatomy&& a) noexcept { if (this != &a) { // don't want to rename, so no copy value = std::move(a.value); } showMsg(4, "obj #" + std::to_string(myCount)); return *this; } /*--- destructor ---*/ Anatomy::~Anatomy() { showMsg(5, "obj #" + std::to_string(myCount)); // name_ & value are composed, so their dtors // are called here } /*--- name setter ----*/ void Anatomy::name(const std::string& nameStr) { name_ = nameStr; } /*--- name getter ---*/ std::string Anatomy::name() const { return name_; } /*--- objNumber getter ---*/ size_t Anatomy::objNumber() const { return myCount; } /*--- value setter ---*/ template<typename T> void Anatomy::setValue(const T& t) { value = t; } /*--- value getter ---*/ template<typename T> T Anatomy::getValue() const { try { return std::any_cast<T>(value); } catch (...) { std::cout << " --- can't retrieve value for " << name_ << " ---"; return T(); } } /*--- Anatomy object factory ---*/ template<typename T> Anatomy makeAnatomy(const T& t = T()) { Anatomy temp("created"); temp.setValue(t); return temp; } /*--- show Anatomy state ---*/ template<typename T> void showAnatomy(const Anatomy& a, size_t line = 0) { if (line > 0) std::cout << "\n at line #" << line; std::cout << "\n " << a.name() << ", obj#" << a.objNumber() << ", has value " << a.getValue<T>() << std::endl; } Using Code: 362 std::cout << "\n Demonstrate Class Anatomy"; 363 std::cout << "\n ===========================\n"; 364 365 Anatomy a1; 366 a1.setValue(1.5); 367 showAnatomy<double>(a1, __LINE__ - 2); 368 369 Anatomy a2{ a1 }; 370 a2.name("a2"); 371 showAnatomy<double>(a2, __LINE__ - 2); 372 373 a1 = a2; 374 showAnatomy<double>(a1, __LINE__ - 1); 375 376 Anatomy a3 = makeAnatomy( std::string("some string") ); 377 showAnatomy<std::string>( a3, __LINE__ - 1 ); 378 379 Anatomy a4; 380 a4 = makeAnatomy(5); 381 showAnatomy<int>(a4, __LINE__ - 1); Output: Demonstrate Class Anatomy =========================== default ctor invoked, obj #1 at line #365 unknown, obj#1, has value 1.5 copy ctor invoked, obj #2 at line #369 a2, obj#2, has value 1.5 copy assignment invoked, obj #1 at line #373 unknown, obj#1, has value 1.5 default ctor invoked, obj #3 move ctor invoked, obj #4 dtor invoked, obj #3 at line #376 created, obj#4, has value some string default ctor invoked, obj #5 default ctor invoked, obj #6 move ctor invoked, obj #7 dtor invoked, obj #6 move assignment invoked, obj #5 dtor invoked, obj #7 at line #380 unknown, obj#5, has value 5 dtor invoked, obj #5 dtor invoked, obj #4 dtor invoked, obj #2 dtor invoked, obj #1
This is the first time we've seen move operations in action. The move constructor, Anatomy(Anatomy&& a), and move assignment operator, Anatomy& Anatomy(Anatomy&& a), transfer ownership of the source instance state to the destination instance. Move Construction : X(X&& x) X&& is an rvalue reference bound to input x. Rvalue references accept either lvalues - named variables - or rvalues - temporaries like X(). Move construction transfers ownership of the state of a temporary X to state for a new X as part of its construction, e.g.: X x2 = std::move(x1); Usually we won't see a move construction written that way. Instead, the move happens when a temporary is returned from a function: X makeAnX() {
  X temp;
  // configure temp
  return temp;
}
X x = makeAnX();
Ownership transfer often amounts to a pointer swap. If we std::move(str), where str is a std::string, ownership transfer passes str's character pointer to the destination string rather than copying all of its characters, and replaces str's character pointer with nullptr. For the makeAnX() example above, the temporary is destroyed immediately after the return, so the str destructor calls delete on its character pointer, causing no harm (because it is nullptr). The compiler will use a copy constructor if the source object is not a temporary or if the source does not define move construction. Move construction allows us to build functions that define complex data structures and return them efficiently, with moves, to the using code.
Move Assignment : X& operator=(X&& x) Move assignment works just like move construction, but is used to transfer ownership to an existing instance: X x;
x = makeAnX();

  1. lvalue and rvalue references were part of the C language lore. An lvalue is a named instance that would be defined on the left side of a declaration, e.g.: int i{5}; Here i is an lvalue. An rvalue is an anonymous instance that would appear on the right side of an expression, e.g.: i = 7; Here the literal 7 is an rvalue. These classifications become slightly more complicated for C++. See Lvalues and Rvalues
If we return by value a temporary instance of Anatomy from a function that creates it, as in makeAnatomy, the compiler uses move construction to return to a newly created instance, as in line 376 of the Anatomy output, above. If we return to an existing instance, as in line 380, the compiler uses move assignment. The std::move(a.name_) in Anatomy's move constructor simply copies the a.name_ character pointer to name_ instead of copying all its characters. The source instance is a temporary and will be destroyed as soon as the return has completed, so that loss of ownership has no effect. We've used, in the Anatomy class, std::any to hold a value it uses. Std::any, added in C++17, will hold an instance of any type. When we call std::move(a.value) that uses the move constructor of the type held by any. If that type does not provide a move constructor, the compiler simply uses the value's copy constructor. Move assigning to an already existing instance works the same way, e.g., transferring ownership of the temporary Anatomy's resource, inside makeAnatomy, to the destination via a move assignment operation.

5.5 Compiler-Generated Methods

C++ compilers are required to generate certain methods if needed to support the C++ object model. Compilers may generate:
  1. copy constructor and copy assignment operator to support value behavior
  2. move constructor and move assignment operator to improve return value performance
  3. destructor to support release of resources
  4. default constructor to support basic creation
How Compiler-Generated Methods Work: All of the compiler-generated methods do member-wise operations on each member of the class and its base classes, e.g., a compiler generated copy constructor simply copies each data member of the class and each data member inherited from its base classes.
For a large fraction of the classes we write, class syntax is really quite simple, because the compiler implicitly generates a lot of methods for us. When that happens is described in Table 2.

Table 2. - Compiler generated methods

SyntaxConditions
X() Default Constructor: Compiler will generate if, and only if, no constructors are declared.
X(const X& x) Copy Constructor: Compiler will always generate as needed if not declared by class.
X(X&& x) Move Constructor: Compiler will generate if, and only if, no copy and no move constructors
and assignment operators are declared.
X& operator=(const X& x) Copy Assignment: Compiler will always generate as needed if not declared by class.
X& operator=(X&& x) Move Assignment: Compiler will generate if, and only if, no copy and no move constructors
and assignment operators are declared.
~X() Destructor: Compiler will always generate if not declared.
The only difficulty with this process is to remember when you should or should not let the compiler generate those methods. That isn't to difficult - the conditions are presented in Table 3., below.

Table 3. - When Value Methods should (not) be implemented

Define Value Methods When Examples
Allow compiler to generate methods Bases and member data have correct copy, assignment, and destruction semantics. All data members in class and its base classes are fundamental types, arrays of fundamental types, or any of the STL container classes.
Designer provides methods At least one base class is not a value type or at least one data member does not have correct copy, assignment, and destruction semantics. Class contains pointer to resources stored on heap.
Designer disables value methods:
  X(const X&X x) = delete;
  X& operator=(const X& x) = delete;
A data member is not copyable or assignable. Data member is const or reference, Y& y. Other examples: data member is mutex, condition variable, database reader, file stream.
You need to (almost always) obey the law of threes: if you provide any one of the three methods: copy constructor, copy assignment operator, and destructor, you should provide all three. There are three cases, as cited in Table 3:
  • Let the compiler generate methods - true for a large fraction of the classes we design.
  • Provide the methods, as we did in Example3::Point.
  • Disable copy and assignment with the = delete suffix.

5.6 Class Examples from the Repositories

In Table 4., below, you will find many more examples of classes. They are listed in order of relevance to the material in this chapter. For Windows, download code you want to study, and, using Windows explorer, open the solution (*.sln) in Visual Studio Community Edition, 2019 or later. Each project needs to be built with the C++17 option set. That should already be set in each project's properties, but if not, simply, right-click on the project properties and select: Properties > C/C++ > language > C++ Language Standard > ISO C++17 Standard Eventually, setup for Visual Studio Code will be included in most of the repositories. That will allow you to run this code in Linux or MacOS. But that won't appear for awhile.

Table 4. - Examples from C++ Repositories

Code SourceDescription
CppStoryRepo.html Contains all code used for illustration in the chapter.
STRCode.html Shows how to implement all of the methods needed to make a value type. The documentation page provides a lot of that information.
CppBasicDemos.html Demonstrates: pointers and references, lambdas, callable objects, storage sizes, modern casts, alternate constructor syntax (Equiv), and class layout.
FileManager.html Contains five projects that illustrate alternate ways of building a directory explorer. Much like the Point projects in Section 4.1, they explore ways of making code flexible and effective.
CppGraph.html Builds a directed graph and provides methods for processing data in each vertex and edge.
ObjectFactories.html Demonstrates code for Dependency Inversion: Interfaces and Object Factories.
STL-Containers.html Simple demonstrations of use for each of the STL containers and adapters.
CppConcurrentFileAccess.html This component attempts to open a file for either reading or writing. If open fails, the component sleeps for a while, then attempts to open again, trying a finite number of times.

5.7 Epilogue

We've looked, in some detail, at C++ classes, the way they are implemented and used, and the support the language provides for building value types. This concludes our study of classes. In the next chapter we explore class relationships - the glue that holds object-oriented programs together. Ciao.

5.8 Programming Exercises

  1. Write a class that has a single implemented method, void title(const std::string& msg). The method should format the title, as in Exercises-1:7.
    How many methods does this class have? Remember that the compiler will generate certain constructors, a copy assignment operator, and a destructor. The answer depends on whether you stored the msg string as a member of the class - no need to do that for a title, but you might want to add other functionality to the class later. Should you implement the compiler generated methods?
  2. For the class you created in the first exercise, add a method called add(const std::string& text) that accepts a std::string and appends it to a private std::string member. Also, add a method to return the std::string member by value.
    How should you initialize the std::string member? Can you write the "add" method so that it returns a reference an invoking instance of the class? That will allow you to chain calls, e.g., x.add(": one").add(", two").
  3. Build a class with method bool top(const std::string& fileSpec, size_t n). Attempt to open the file, and if that succeeds, display the first N lines.
    Please handle file opening errors in some meaningful way.
  4. Using code from the FileDates repository, build a class that finds all the files within a specified range of dates.
    What you are asked to do is fairly easy to implement, so take the time to look carefully at the design of the FileDates compound object.

5.9 References

cpppatterns.com
Posts on Fluent C++
C++ Idioms
C++ weekly videos
  Next Prev Pages Sections About Keys