CppStory Repo

Chapter #2 - C++ Survey

data, operations, classes, templates, libraries

2.0 Survey Prologue

This chapter gives a quick look at the programming facilities C++ and its libraries provide. Subsequent chapters cover each topic in detail, with examples. 0.75rem Discussing a programming language without using terms not yet introduced is hard to avoid. This chapter presents most of the basic ideas up front, so that later chapters can reference constructs and concepts before covering their full details - and you can follow along with confidence that those details will come.

2.1 Data Types

C++ provides fundamental types built into the language, additional types from the standard libraries, and full support for user-defined types. User-defined types can be designed to behave essentially like fundamental types. Fundamental types:
void, bool, nullptr_t, integral, char, and floating types
  • void - only type with no values
  • bool - values true and false
  • std::nullptr_t - type of the nullptr literal
  • integral types: int with qualifiers: short, long, long long, unsigned, const, volatile
    std::size_t, std::size_type
  • character types: char, wchar_t with qualifiers: signed, unsigned, const, volatile
    unicode characters: char16_t, char32_t, char8_t (C++20) , with qualifiers: const, volatile
  • floating point types: float, double with qualifiers: long (double only), const, volatile
References: cppreference.com/types Examples: void demoFundamentalTypes() { showTitle("Demo fundamental types"); int i{ 3 }; char c{ 'z' }; double d{ 3.1415927 }; bool b{ true }; std::cout << "\n value of integer i = " << i; std::cout << "\n value of character c = " << c; std::cout << "\n value of double d = " << d; std::cout << "\n value of bool b = " << b; std::cout << "\n value of nullptr = " << nullptr; std::cout << "\n numerical value of nullptr = " << static_cast<size_t*>(nullptr); std::cout << "\n"; } Demo fundamental types ------------------------ value of integer i = 3 value of character c = z value of double d = 3.14159 value of bool b = 1 value of nullptr = nullptr numerical value of nullptr = 00000000
Arrays and pointers:
arrays
An array is a fixed size sequence of continguous elements, all of the same type. T arr[N];  [ declaration of array arr ] T t = arr[1];
T is any default constructable C++ type, N is a compile-time constant.
t contains the value of the second element of the arr array
Example: const char* args[] = { "one", "two", "three" }; size_t sizeOfArgsArray = 3; std::cout << "\n displaying args[]"; std::cout << "\n "; for (size_t i = 0; i < sizeOfArgsArray; ++i) { std::cout << args[i] << " "; } Output displaying args[] one two three
pointers
A pointer is a reference to the memory location of a variable, to which it is bound.
X x;
X* pX = &x;  
[& on right of assignment is an address]
pX is a pointer variable containing the address of x
++pX increments address stored in pX by sizeof(X)
Retrieve value referenced by pX using dereference operator, *pX, which returns the value of x.
Fig 1. Pointer to x ε X
Pointers are often used to manage data stored on the C++ native heap: Manually allocating storage on heap double* pDbl = new double[2] {1.5, -4.3}; std::cout << "\n 1st element on heap = " << *pDbl; std::cout << "\n 2nd element on heap = " << *(pDbl + 1) << std::endl; delete[] pDbl; // allocator has to remember array size and remember to delete when // array is no longer needed. Code like this rarely appears outside a class that carefully manages allocation and deallocation. Subsequent chapters cover this in detail. One safe approach is the standard smart pointers, std::unique_ptr and std::shared_ptr. Using std::unique_ptr /*-- std::unique_ptr to scalar uses pointer syntax --*/ displayDemo("--- creating double on heap with unique_ptr ---"); auto ptr0 = std::unique_ptr<double>(new double{ -1.5 }); std::cout << "\n element on heap = " << *ptr0 << std::endl; ptr0.release(); /*-- std::unique_ptr to array uses array syntax --*/ displayDemo("--- creating small array of doubles on heap with unique_ptr ---"); auto ptr1 = std::unique_ptr<double[]>(new double[2]{ -1.5, 3.2 }); std::cout << "\n 1st element on heap = " << ptr1[0]; std::cout << "\n 2nd element on heap = " << ptr1[1] << std::endl; ptr1.release(); /*-- see references at end of chapter for unique_ptr --*/ This is safer than manual allocation. The heap allocation is always returned. Without calling release(), the allocation returns automatically when the unique_ptr goes out of scope.
Example:  Reading command line arguments This example displays command line arguments on the console using main's argv[] array, then again using equivalent pointers. Example Code int main(int argc, char* argv[]) { std::cout << "\n displaying command line arguments using array"; std::cout << "\n "; for (int i = 0; i < argc; ++i) { std::cout << argv[i] << " "; if ((i + 1) % 2) } std::cout << "\n "; } std::cout << "\n "; std::cout << "\n displaying command line arguments using pointer"; std::cout << "\n "; char** ptr = argv; for (int i = 0; i < argc; ++i) { std::cout << *(ptr++) << " "; if ((i + 1) % 2) std::cout << "\n "; } std::cout << "\n\n "; } Output // displaying command line arguments using array chapter1-survey.exe /P .. /p *.h;*.cpp /R ^template /H displaying command line arguments using pointer chapter1-survey.exe /P .. /p *.h;*.cpp /R ^template /H
C++ uses references to pass function arguments by reference:
C++ references X& xr = x;  [ declaration of reference xr bound to x ] Unlike pointers, C++ reference types cannot be reset. The instance referred to, x, is fixed at the reference declaration and cannot change. The value of xr is always the value of x, which can be changed. Think of a reference as another name for the referenced instance. References serve most often to pass arguments to a function by reference. Passing an argument by value, f(T t), copies the argument's value into the function's stack frame. Passing by reference, f(T& t), places a small reference in the function's stack frame bound to the parameter in the caller's scope - avoiding the copy of what could be a much larger object.
The C++ standard libraries provide a large set of pre-defined types:
STL sequential and associative containers, container adapters sequential containers - linear sequence of elements:
string, array, vector, deque contiguous memory storage, can be indexed
forward_list, list nodes allocated on heap, cannot be indexed
associative containers - key based storage on heap:
set, multiset, unordered_set, unordered_multiset key only storage using balanced binary tree or hash table
map, multimap, unordered_map, unordered_multimap key-value storage using balanced binary tree or hash table
container adapters:
queue, stack sequential containers accessible only from end(s)
priority_queue constant time lookup of largest element, logarithmic insertion and extraction
References: cppreference.com/container
STL-Containers.html
Example: Sum elements in container using std::vector and std::for_each This example uses std::vector<int> and the for_each algorithm. It also uses lambdas, so you may want to peek at the lambda discussion at the end of the Operations section first. Example Code std::vector<int> test{ 1, 2, 3, 4, 5 }; std::string prefix = "\n "; auto show = [&](auto element) { // peek at lambda, below std::cout << prefix << element; prefix = ", "; }; std::for_each(test.begin(), test.end(), show); int sum = 0; auto sumer = [&sum](auto element) { sum += element; }; std::for_each(test.begin(), test.end(), sumer); std::cout << "\n sum = " << sum; Output 1, 2, 3, 4, 5 sum = 15
Special containers, streams, and other types
special containers:
pair contains two elements which may have distinct types
tuple contains finite number of elements with distinct types
initializer_list contains sequence of elements, all of the same type normally filled with initialization list, e.g., { 1, 2, 3, ... }
any holds value of any type, provides std::any_cast for retrieval
optional used to return values or signal failure to return
variant similar to any, but only holds values from a specified set of types
stream types:
istream sends sequence of values of fundamental types to console using insertion operator<<, which may be overloaded for user-defined types.
ostream retrieves sequence of values of fundamental types from keyboard using extraction operator>>, which may be overloaded for user-defined types.
ifstream retrieves sequence of values of fundamental types from attached file using extraction operator<<, which may be overloaded for user-defined types.
ofstream retrieves sequence of values of fundamental types from attached file using extraction operator>>, which may be overloaded for user-defined types.
istringstream retrieves sequence of values of fundamental types from attached in-memory string using extraction operator<&lot;, which may be overloaded for user-defined types.
ostringstream retrieves sequence of values of fundamental types from attached in-memory string using extraction operator>>, which may be overloaded for user-defined types.
other types:
unique_ptr<T> Construction allocates instance t ε T, destruction deallocates t. Assignment moves ownership.
shared_ptr<T> Reference counted smart pointer, assignment adds counted reference.
exception When code error occurs exception instance is "thrown", then handled by catch clause associated with enclosing try block.
chrono Class for managing time durations and dates.
References: cppreference.com/header
Example: Write file using file stream and std::optional std::optional<T> returns a computed value when available. When computation fails - a file open failure, for example - it returns nullptr. std::ofstream ofstrm; /*--- attempt to open file for writing ---*/ auto fout = [&ofstrm](const std::string& filename) { ofstrm.open(filename, std::ios::out); std::optional<std::ofstream*> opt = &ofstrm; if (!ofstrm.good()) opt = nullptr; return opt; }; /*--- attempt to write file ---*/ auto opto = fout("testWrite.txt"); if (opto.has_value()) { std::string test("\n this is a test\n"); auto foptr = opto.value(); *foptr << test; foptr->close(); }
The details dropdown below summarizes the sizes of many common types.
Type instance sizes Value Sizes -- limits -- bits in byte = CHAR_BIT = 8 min value of char = CHAR_MIN = -128 max value of char = CHAR_MAX = 127 min value of int = INT_MIN = -2147483648 max value of int = INT_MAX = 2147483647 min value of float = FLT_MIN = 1.17549e-38 max value of float = FLT_MAX = 3.40282e+38 min value of double = DBL_MIN = 2.22507e-308 max value of double = DBL_MAX = 1.79769e+308 -- integral types -- 4 = size of std::nullptr_t 1 = size of enum std::byte 1 = size of bool 1 = size of char 1 = size of signed char 1 = size of unsigned char 2 = size of char16_t 4 = size of char32_t 2 = size of short 4 = size of int 4 = size of unsigned int 4 = size of long 4 = size of unsigned long 8 = size of __int64 <==> long long int 8 = size of unsigned __int64 <==> unsigned long long int 4 = size of unsigned int <==> size_t --------------------------------- demonstrate integer roll-over: i = UINT_MAX : 4294967295 i + 1 = 0 --------------------------------- -- character types -- 1 = size of char 1 = size of unsigned char 2 = size of wchar_t 2 = size of char16_t 4 = size of char32_t -- float types -- 4 = size of float 8 = size of double 8 = size of long double -- pointers and references -- 4 = size of double * 8 = size of double, note: is double& 8 = size of double <==> sizeof(double&) pDouble: 00EFFCCC --> 15727820 ++pDouble: 00EFFCD4 --> 15727828 -- arrays -- 16 = size of int const [4] -- std::strings -- 28 = size of class std::basic_string<char,struct std::char_traits<char>,class std::alloc..., is std::string{} 28 = size of class std::basic_string<char,struct std::char_traits<char>,class std::alloc..., is std::string{ "a std::string" } -- structs -- 1 = size of struct `void __cdecl demoCompoundTypes(void)'::`2'::Empty, is struct {} 24 = size of struct `void __cdecl demoCompoundTypes(void)'::`2'::Struct, is struct { 1, 1.5, "a literal string" } -- function pointers -- executing testFun1(const std::string&) 4 = size of void (__cdecl*)(class std::basic_string<char,struct std::char_traits<char>,... executing testFun2() 4 = size of class std::basic_string<char,struct std::char_traits<char>,class std::alloc... -- vector<double> -- 16 = size of class std::vector<double,class std::allocator<double> >, is std::vector<double>{} 16 = size of class std::vector<double,class std::allocator<double> >, is std::vector<double>{ -0.5, 0, 0.5, 1.0, 1.5 } -- vector<double>::iterator -- 12 = size of class std::_Vector_iterator<class std::_Vector_val<struct std::_Simple_type..., is vector<double>::iterator -- unordered_map<std::string, int> -- 40 = size of class std::unordered_map<class std::basic_string<char,struct std::char_trai..., is std::unordered_map<std::string, int>{} 40 = size of class std::unordered_map<class std::basic_string<char,struct std::char_trai..., is std::unordered_map<std::string, int>{ {"one", 1}, ... } -- unordered_map<std::string,int>::iterator -- 12 = size of class std::_List_iterator<class std::_List_val<struct std::_List_simple_typ..., is std::unordered_map<std::string, int>::iterator -- display vector contents in rows of N -- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- miscellaneous types -- 1 = size of enum std::byte 4 = size of unsigned int, is size_type 48 = size of class std::tuple<int,double,class std::basic_string<char,struct std::char_t... 4 = size of class std::unique_ptr<double,struct std::default_delete<double> > 8 = size of class std::shared_ptr<double> 8 = size of class XwithDouble <==> instance of class with double member 4 = size of class XwithRef <==> instance of class with double& member 8 = size of class XwithDouble <==> class with double member 4 = size of class XwithRef <==> class with double& member 4 = size of std::nullptr_t 80 = size of class std::basic_ostream<char,struct std::char_traits<char> > <==> std::cout 80 = size of class std::basic_ostream<char,struct std::char_traits<char> > <==> std::ostream The meaning of life is 42 4 = size of class <lambda_cb1c8eb6ab7293ad79a4f9e6fdc0cd2e> 28 = size of class std::basic_string<char,struct std::char_traits<char>,class std::alloc... 4 = size of char const * content of a std::string string size = 25 allocation size = 31

2.1.1 User Defined Types

User defined types: enums, structs, classes, and type aliases An enum is a sequence of named integral values. Enums serve most often as case selectors in switch statements.
scoped enum: enum class E { ... };
enum class Color { red, green, blue }; Color color; --- if(color == Color::red) { doRedThing(); } ---
Structs are identical to classes except that struct members are public by default while class members are private by default. Structs most often serve as an implementation detail holding a collection of values with heterogeneous types.
struct S { ... };
struct S { int i; double d; }; S s { 2, 3.1415927 }; int j = s.i; double e = s.d;
Classes typically build program abstractions - a class represents a domain entity like an order or product. Classes also build implementation abstractions like blocking queues and thread pools. Later chapters cover both of those.
class C { ... };
class X { public: void set(const std::string& s); std::string get(); private: std::string str; };
A type alias provides another name for an existing type. They are surprisingly useful for making code readable. For example, a type alias fileName for std::string signals to readers that the string holds a file name. The alias can appear anywhere the language accepts a std::string, such as a function parameter type.
type alias: using AliasName = Sometype;
Aliases give application-specific domain names to standard types and provide short names for template types with many template parameters. alias - c++11:
using VecStr = std::vector<std::string>;
alias - C++98:
typedef std::vector<std::string> VecStr;
Compile-time type testing lets us build very flexible template functions and methods. The section below and Chapter #7 - Templates cover this in detail. Run-time type testing is less common, but lets us display the name of a variable's type and test whether two instances share the same type.
Testing types at compile-time and run-time Compile-time tests use template meta programming, covered in detail in Chapter 8. This appears here because you will encounter it before reaching those details. compile-time type tests:   std::type_traits
template<typename T>
void display(const T& t) {
  if constexpr (std::is_fundamental<T>::value) {
  // do display operations consistent with fundamental types
  // std::is_fundamental<T> is a std::type-trait
}
We can also create user-defined type-traits, as discussed in Chapter #8 - Template Metaprogramming.
run-time type tests:   std::type_info
if(typeid(x1) == typeid(x2)) {
  // do something knowing that x1 and x2 have the same types
  // the typeid operator returns a type_info instance. that's what we are comparing.
}
The next chapter uses std::type_info to display type information in several examples.

2.1.2 Definitions and Declarations

A type declaration associates a variable name with a type.
type declaration A type declaration associates a name with a specified type - defining the set of operations the named entity supports - but allocates no storage unless it is also a definition. Storage must be defined later, before the entity is used. extern int x; The entity x supports all int operations, but this statement defines no storage. void f(int i); Declares f as a function to be defined later, accepting a single integer argument and returning nothing. class X; Declares X as a class to be defined later. X is an incomplete type, and this statement is a forward declaration. class Y { /* operations elided */ }; This is commonly called a definition of class Y, but technically it is a declaration: it defines the size required for instances of Y but allocates no storage.
A type definition is a declaration that also allocates, and may initialize, storage for a variable.
type definition A type definition allocates storage for a declared entity of a specified type. The designer specifies the storage location: static, stack, or heap memory. int x; int y{ 2 }; The variable x is declared and storage is allocated in the local stack frame, filled with a default value.
The variable y is declared and storage is allocated in the local stack frame, initialized with the value 2.
Life-time of this storage extends from the point of declaration until the thread of execution leaves the current scope.
void f(int i) { std::cout << "\n " << argument has value << i; } The code within the { and } scope delimiters compiles and allocates to static memory adjacent to other program code.
Life-time of this allocation spans from process creation until the process begins termination.
X x; Y y(...); Allocates storage for x ε X in the local stack frame and initializes it with a default value.
Allocates storage in the local stack frame for y ε Y and initializes it using a Y constructor that accepts the type(s) specified by the ... ellipsis above.
Life-time of this storage extends from the point of declaration until the thread of execution leaves the current scope.
static T t; where T is some fundamental or user-defined type Allocates storage for variable t in static memory, adjacent to other code for the program.
Life-time of the memory reservation for t extends from first use to the end of program execution.
X* pX = new X; Allocates storage for the pointer pX in the local stack frame and allocates storage in the native heap for an unnamed instance of X. The storage for pX receives the heap address of the X instance, and X's default constructor initializes the instance storage.
Life-time for this storage extends from the time the new operator is invoked until delete operator is called on pX.
Chapter #3 - Data provides examples and covers many details about C++ data types.

2.2 Operations

A C++ program is an ordered sequence of statements, where each statement is an expression followed by a semicolon ";". Expressions may be declarative, operational, iterative, selection, or try-block types, and may combine these. Declarative statements are compile-time artifacts that disappear after compilation. All other types generate code that executes at run-time.
program scopes
C++ programs are partitioned by scopes, e.g., sequences of statements enclosed within braces "{ " and "}". There are several types of scopes:
  • namespace N { ... }
  • class C { ... };
  • struct S { ... }
  • enum class E { ... }
  • void f(int i) { ... }
  • try { ... } catch(execption& ex) { ... }
  • for(startExpression; endExpression; incrementExpression) { ... }
  • for(T t : container) { ... }  T may be replaced by auto
  • while(predicate) { ... }
  • do { ... } while(predicate);
  • if(predicate) { ... } else { ... }
The first four are compile-time artifacts that define names, allowed operations, and accessibility. They disappear after compilation.
The rest are compile-time and run-time constructs that affect execution and flow. Any entity declared within these scopes has a life-time that starts at the point of declaration and ends when the thread of execution leaves the scope.
This section below discusses all of these run-time operations.
C++ provides unary and binary operations for fundamental types, and operator functions that can be overloaded for user-defined types. The most commonly used operations and operators for both fundamental and user-defined types appear below:
dereference and address of:   * &
struct X { Y y; ... }; X x1; Y y;
X* pX = &x1;
sets pointer pX ε X* to address of instance x1 ε X
X x2 = *pX;
instance x2 ε X gets copy of contents of x1 pointed to by pX by dereferencing pointer, pX and copying value to x2
Fig 2. Pointer to x ε X
member access operations:   . ->
struct X { int y; ... };
X x{ 2, ... }; X* pX = &x;
x.y, pX->y both refer to value of y, e.g., 2, contained in struct X  
pre increment and decrement:   ++i --i
int i{ 0 }; ++i; --i;
++i increments i and returns value 1
--i decrements i and returns value 0  
post increment and decrement:   i++ i--
int i{ 0 }; i++; i--;
i++ increments value to 1 and returns prior value 0
i-- decrements value to 0 and returns prior value 1  
logical operators:   == != < <= >= > && || !
Here, we illustrate how a user defined type could implement
two of these logical operations:
struct X {
  int i; double d;
  bool operator==(const X& x) {
    return i == x.i && d == x.d;
  }
  bool operator!=(const X& x) {
    return i != x.i || d != x.d;
  }
};
X x1{ 1, 1.5 }, x2{ 1, 1.5 }; x3{ 2, -0.5 }
x1 == x2; x1 != x3;
Both statements above are true.
index operator:   a[]
double a[] { 1.0, 1.5, 2.0 };
a[1] == 1.5 has value true
arithmetic operations:   + - * /
int x1{ 2 }; int x2{ 4 };
x1 + x2 == 6; x1 - x2 == -2; x1 * x2 == 8; x2 / x1 == 2;  
All of the boolean expressions above are true;
loops:  for, while, do
for(int i=0; i<Max; ++i) {
Any, or all, of the three loop conditions may be omitted, provided that omitted conditions are defined. If no termination condition is provided, the loop requires a conditional break statement to terminate.
Loop operations elided
}
for(auto item : collection) {
Collection must provide an iterator and methods begin() and end() which return iterators referring to the first element and one past the last of its elements.
Loop operations elided
}
while(predicate) {
Loop operations will not be executed if predicate is false on entry.
Loop will continue until operations in scope of the loop make its predicate false.
Loop operations elided
}
do {
Ensures that loop operations are executed at least once.
Loop will continue until operations in scope of the loop make its predicate false.
Loop operations elided
} while(predicate);
selection:  if-else, ternary operator, switch
if(predicate) {
Operations to execute when predicate is true go here.
}
else {
Operations to execute when predicate is false go here. This else clause is optional.
}
The ternary operator: predicate ? e1 : e2 returns the value of expression e1 if predicate evaluates to true, otherwise it returns the value of expression e2.

The switch operation enables execution of code specific to some specified case.
switch selectors, iSelect, and switch cases, iSelect1, iSelect2, ... belong to a list, IS, of integral selectors, e.g., distinct integers or values of an enumeration: iSelect, iSelect1, iSelect2, ... ε IS
switch(iSelect) {
case iSelect1:
Operations for iSelect1 case go here.
  break;
case iSelect2:
Operations for iSelect2 case go here.
  break;
  ----
default:
Operations when iSelect does not match any declared iSelect[n] case go here.
  break;
}
function call operator:   f()
void f(const std::string& str)
{
  std::cout << "\n " << str;
}
f("hello Syracuse"); displays message on console  
method call operator:   operator()
class X {
  void operator()(const std::string& s) {
    std::cout << "\n " << s;
  }
};
X x;
x("hello");
x.operator()("hello");
The last two statements are equivalent. We call X a functor because an instance can be invoked like a function, as above.  
Functions can be defined at namespace scope, including the global namespace, but not in function scope - there are no inner functions. Methods (functions bound to a specific class) are defined in class scope.
Lambdas are locally defined callable objects useful for starting threads and using STL algorithms.
Lambda:   [/* capture */](/* args */) { /* code block */ };
char ul = '-'; auto makeTitle = [ul](const std::string& title) { std::cout << "\n " << title; std::cout << "\n " << std::string(title.size() + 2, ul); }; makeTitle("demonstrate lambda"); emits: demonstrate lambda -------------------- [ul] in the second code line, captures the value of local variable ul from the local scope and uses it for an underline character, below the title message. The title is passed in as a parameter when lambda, makeTitle, is invoked.
Lambdas can be defined in namespace, function, and method scopes, serving the role of inner functions. Chapter #4 - Operations covers the many other uses of lambdas.

2.3 Classes and Class Relationships

C++ classes and structs are identical except that struct members are public by default and class members are private by default. With either, the designer marks specific members public with a "public:" declaration and others private with "private:".
By convention, structs aggregate data, much like std::tuple. They also define code interfaces, as described in Chapter 4. Classes implement abstractions defined by their public members. A well-designed class gives only its methods direct access to the data it manages, enabling strong guarantees about data validity.
Object Oriented Design uses classes and class relationships to structure program activities. Basic classes can be quite simple, defining only methods for managing and accessing class state. The language also provides design facilities for value or reference type behavior. Chapter #5 - Classes covers those details.
basic class
The Person class declares personal data contained by each instance. These "stats" hold name, occupation, and age in a std::tuple.
Person instances support copy and assignment because the only data member, personStats, is a std::tuple whose elements all have correct copy, assignment, and destruction semantics.
Compiler-generated methods for copy, assignment, and destruction operate on each base class and each composed member. Here those generated operations delegate directly to std::tuple's copy, assignment, and destruction operations.
class Person {
public:
  using Name = std::string;
  using Occupation = std::string;
  using Age = int;
  using Stats = std::tuple<Name, Occupation, Age>;

  Person();
  Person(const Stats& sts);
  Stats stats() const;
  void stats(const Stats& sts);
  bool isValid();
  Name name() const;
  Occupation occupation() const;
  void occupation(const Occupation& occup);
  Age age() const;
  void age(const Age& ag);

private:
  Stats personStats;
};
                          
Person defines four type aliases that make the class code readable, easier to test, and easier to use.
It provides getter and setter methods for occupation and age, which should be changeable, and only a getter for name, which should not change.
References: CppStory Repository
Fig 1. Person Class code layout
Looking back at the Person class in the previous details, two things stand out. Two functions share the name Person, and neither has a return value. C++ classes define constructors to build initialized instances, and the language recognizes any function with the class name and no return value as a constructor.
The language specifies that constructors return no value - not even void. To handle two or more functions with the same name, C++ provides function overloading.
function overloading
Function overloading defines two or more functions with the same name but different sequences of argument types1.
Consider the two Person constructors from the previous "basic class" details:
  Person();
  Person(const Stats& sts);
The second accepts an sts argument to initialize an internal data member. The first accepts no argument, so the internal data member gets a default initialization.
C++ classes need to initialize instances in different ways, and since all constructors share the same name, the compiler must distinguish between them.
The compiler does this by concatenating the function name with its argument types2 to form an internal name. This process is called name-mangling, and it is what enables function overloading.
Most class declarations use function overloading, but designers can apply overloading for other purposes as well.
  1. Return types play no role in function overloading.
  2. Actually, the name consists of a tokenized sequence that identifies the function name and argument types in a compact format.
function overloading example
  /*---- find first and last elements of collection, may throw ----*/

using PD = std::pair<double, double>

PD firstAndLast(double dArr[], size_t N) {
  if (N < 1)
    throw std::exception("no contents in array");
  return PD{ dArr[0], dArr[N - 1] };
}

using PI = std::pair<int, int>

PI firstAndLast(const std::vector<int>& vecInt) {
  if (vecInt.size() < 1)
    throw std::exception("no contents in vector");
  return PI{ vecInt[0], vecInt[vecInt.size() - 1] };
}

/*---- find first and last elements of collection, using optional ----*/

std::optional<PD> firstAndLastOpt(double dArr[], size_t N) {
  std::optional<PD> opt;
  if(N > 0)
    opt = std::pair{ dArr[0], dArr[N - 1] };
  return opt;
}

std::optional<PI> firstAndLastOpt(const std::vector<int>& vecInt) {
  std::optional<PI> opt;
  if (vecInt.size() > 0)
    opt = std::pair{ vecInt[0], vecInt[vecInt.size() - 1] };
  return opt;
}

/*---- demonstrate first and last ----*/

int main() {

  std::cout << "\n  Demonstrating Function Overloading";
  std::cout << "\n ====================================\n";

  double dArr[]{ 1.5, -0.5, 3.0 };
  std::vector<int> vecInt{ 1,2,3,4,5 };

  try {
    auto [firstd1, lastd1] = firstAndLast(dArr, 3);
    std::cout << "\n  first = " << firstd1 << ", last = " << lastd1;

    auto [firsti1, lasti1] = firstAndLast(vecInt);
    std::cout << "\n  first = " << firsti1 << ", last = " << lasti1;
  }
  catch (std::exception & ex) {
    std::cout << "\n  " << ex.what() << std::endl;
  }

  std::optional<PD> optd = firstAndLastOpt(dArr, 3);
  if (optd.has_value()) {
    auto [firstd2, lastd2] = optd.value();
    std::cout << "\n  first = " << firstd2 << ", last = " << lastd2;
  }
  else {
    std::cout << "\n  array query failed";
  }

  std::optional>PI< opti = firstAndLastOpt(vecInt);
  if (opti.has_value()) {
    auto [firsti2, lasti2] = opti.value();
    std::cout << "\n  first = " << firsti2 << ", last = " << lasti2;
  }
  else {
    std::cout << "\n  vector query failed";
  }
  
  std::cout << "\n\n";
}
C++ classes support five relationships: inheritance, composition, aggregation, using, and friendship. Most object oriented languages support the first four.
class relationships
The five relationships between C++ classes are:
  1. Inheritance: a specialization of a base class by another derived class.
  2. Composition: a permanent relationship between the composer and composed.
  3. Aggregation: a temporary relationship between the aggregator and aggregated.
  4. Using: a non-owning relationship. The used is made available to the user by passing as a reference argument in a class method.
  5. Friendship: a relationship granted by a class to one specific friend that allows the friend access to the class's private members. Friendship weakens encapsulation and so is used only when necessary, e.g., rarely.
Person Class Example Fig 1. Person Class Hierarchy The Person class from the previous example has been expanded into a class hierarchy representing software development roles a person might assume. A class hierarchy like this could form the basis of a Project Management Tool that tracks project progress and the contributions of individual team members.
There are several types of classes in this hierarchy:
  • Interfaces: IPerson and ISW-Eng
  • An abstract class: SW-Eng
  • Concrete classes: Person, Dev, TeamLead, ProjMgr, Project, Baseline, Documents, and Budget
Interfaces IPerson and ISW-Eng decouple the hierarchy from its users, so that changes to any class don't require clients to change, as long as the interface stays the same.
The abstract class SW-Eng provides shared code and types to all its derived classes.
The concrete classes Dev, TeamLead, and ProjMgr represent roles a Software Engineer may assume during a project. The Person class is the key abstraction; all other parts assign one or more roles to a person: developer, team lead, or project manager.
All relationships between these classes are based on inheritance.
The ProjMgr class aggregates a Project instance, letting a manager move to another project when the current one completes.
The Project class composes a Budget since budget is an integral part of the project. It aggregates Documents and code Baseline because those don't exist at project start.
Developers and Team Leads use the Baseline by contributing additions, but hold no ownership - only the Project Manager can authorize deletions and creation of major new parts.
Inheritance lets derived classes share base class code. Useful as that is, inheritance's most important feature is substitution: derived class pointers and references can replace a base class pointer or reference anywhere a function accepts one.
function overriding
Suppose the SW-Eng abstract class from the previous "class relationships" details defines a virtual void doWork() method.
Devs, TeamLeads, and PrjMgrs each have distinct work behaviors, so each needs its own definition of doWork. Virtual function overriding lets us specify those different behaviors.
Virtual function overriding means each derived class provides its own definition of the function, executed based on the derived class type - Dev, TeamLead, or PrjMgr. The overriding function in each derived class must have the same signature, e.g., void doWork(), and overriding applies only to functions declared virtual in the base class.
function overriding example
Example Code /*--- abstract base class ---*/ class SWDev { public: using WorkItems = std::vector<std::string>; SWDev(const std::string& name) : name_(name) {} virtual ~SWDev() {} virtual void doWork() = 0; void getCoffee(); void name(const std::string& nm); std::string name(); protected: std::string name_ = "anonymous"; static WorkItems workItem; }; /*--- shared data ---*/ inline SWDev::WorkItems SWDev::workItem = { "process email", "pull requests for today's work", "work off bugs", "add new features", "write up developer evaluations", "write up project stories", "schedule agile meeting", "discuss requirements with customer", "go golfing with customer" }; inline auto show = [](const std::string& task) { std::cout << "\n " << task; }; /*--- non-virtual functions, don't override ---*/ inline void SWDev::name(const std::string& name) { name_ = name; } inline std::string SWDev::name() { return name_; } inline void SWDev::getCoffee() { std::cout << "\n get coffee from cafeteria, chat"; } /*--- derived classes override virtual do work ---*/ class Dev : public SWDev { public: Dev(const std::string& name) : SWDev(name) {} virtual void doWork() override { show("\n Dev: " + name()); getCoffee(); show(workItem[0]); show(workItem[1]); show(workItem[2]); show(workItem[3]); } }; class TeamLead : public SWDev { public: TeamLead(const std::string& name) : SWDev(name) {} virtual void doWork() override { show("\n TeamLead: " + name()); getCoffee(); show(workItem[0]); show(workItem[5]); show(workItem[6]); show(workItem[1]); show(workItem[2]); show(workItem[3]); } }; class ProjMgr : public SWDev { public: ProjMgr(const std::string& name) : SWDev(name) {} virtual void doWork() override { show("\n Project Mgr: " + name()); getCoffee(); show(workItem[0]); show(workItem[4]); show(workItem[7]); show(workItem[8]); } }; Using Code #include "Overriding.h" int main() { std::cout << "\n Demonstrating Overriding"; std::cout << "\n ==========================\n"; /* form project team */ ProjMgr Frank("Frank"); TeamLead Ashok("Ashok"); Dev Joe("Joe"); Dev Charley("Charley"); Dev Sue("Sue"); TeamLead Ming("Ming"); Dev Barbara("Barbara"); Dev Samir("Samir"); std::vector<SWDev*> ProjectTeam { &Frank, &Ashok, &Joe, &Charly, &Sue, &Ming, &Barbara, &Samir }; /* get to work */ show("Monday, starting work"); for (auto swDev : ProjectTeam) { swDev->doWork(); } show("\n That's all Folks\n\n"); } Output Demonstrating Overriding ========================== Monday, starting work Project Mgr: Frank get coffee from cafeteria, chat process email write up developer evaluations discuss requirements with customer go golfing with customer TeamLead: Ashok get coffee from cafeteria, chat process email write up project stories schedule agile meeting pull requests for today's work work off bugs add new features Dev: Joe get coffee from cafeteria, chat process email pull requests for today's work work off bugs add new features Dev: Charley get coffee from cafeteria, chat process email pull requests for today's work work off bugs add new features Dev: Sue get coffee from cafeteria, chat process email pull requests for today's work work off bugs add new features TeamLead: Ming get coffee from cafeteria, chat process email write up project stories schedule agile meeting pull requests for today's work work off bugs add new features Dev: Barbara get coffee from cafeteria, chat process email pull requests for today's work work off bugs add new features Dev: Samir get coffee from cafeteria, chat process email pull requests for today's work work off bugs add new features That's all Folks
Chapter 5 covers the details of techniques used in this example.
Most C++ projects use classes extensively. Nearly all code implements classes or works with class instances. Chapter 5 examines and dissects examples.

2.4 Templates and Specialization

C++ templates define library functions and classes that depend on one or more unspecified types. STL containers like std::vector<T> are examples.
Template functions and classes must be instantiated with specific types before use, for example: std::vector<std::string>.
template functions
The template function max<T, T> accepts two arguments of the same type and returns the larger of the two. template <typename T>
T max(T t1, T t2) {
    return t1 > t2 ? t1 : t2;
}
When instantiated with a specific type, this function compiles successfully only if T defines operator>(T& t). Otherwise, compilation fails.
There is a subtle problem with max<T, T>. It works correctly for fundamental types and classes like std::string. But consider this invocation:
const char* pStr = max<"aardvark", "zebra">;
The function compiles, but the result may not be what you expect. max<const char*, const char*> compares pointers, returning the one pointing to the higher memory address - not the lexicographically larger string.
The fix is to overload max<T, T> for that specific case, shown below.
overloading template functions
Here's the original max<T, T> declaration: template <typename T>
T max(T t1, T t2) {
    return t1 > t2 ? t1 : t2;
}
and here is a function overload declaration for the case of const char*: using pStr = const char*;
template <>
pStr max(pStr s1, pStr s2) {
    return ((strcmp(s1,s2)>0) ? S1 : s2);
}
The C++ language guarantees the compiler applies the most specific function overload. For const char*, pStr is more specific than the template type T, so the second version compiles and returns the lexicographically larger string, which is what the C function strcmp produces.
This guarantee matters: it means a generic template can coexist with specific overloads for any types that are problematic.
This is not a perfect solution - the set of problematic types may be open-ended - but additional overloads can be provided as new problem types are encountered.
template classes
C++ classes can also be declared as templates. Here's an example:
template <typename T>
class stack {
public:
    void push(T t);
    T pop();
    T top();
    std::size_type size();
private:
    // data members elided
};
template <typename T>
void stack<T>::push(T t) {
    // details elided
}
// other member function definitions elided
With this template declaration we can define:
stack<int> intStk;
stack<std::pair<std::string, int>> prStk;
stack<Widget> WdgStk;
Without templates, each type would require its own stack class declaration. Templates let the compiler do that work automatically.
For each instantiation, the compiler generates a class for that specific type. Each declaration above defines a distinct class: stack<int> is not the same type as stack<std::pair<std::string, int>> prStk;
template class specialization
Suppose some stack<T> methods fail to compile or produce incorrect results for the user-defined type Widget. This can happen when the stack provides copy and assignment operations, but Widget instances are not copyable or assignable, or those operations are incorrect due to an incomplete design1.
Template specialization fixes that problem, in the same way that overloading max<T, T> handled the const char* case. For the Widget specialization, the stack<Widget> copy and assignment operations are declared deleted with =delete postfix qualifiers.
The generic class is defined as:
template <typename T>
class stack {
public:
  stack();
  stack(const stack<T>& stk);
  stack<T>& operator=(const stack<T>& stk);
  void push(T t);
  T pop();
  T top();
  std::size_type size();
private:
  // data members elided
};
template <typename T>
void stack<T>::push(T t) {
  // details elided
}
// other member function definitions elided
stack<Widget> WdgStk; // fails to compile or exhibits incorrect operation.
So, we define a stack class specialization for Widgets like this:
template <>
class stack<Widget> {
public:
  stack();
  stack(const stack<T>& stk) = delete;
  stack<T>& operator=(const stack<T>& stk) = delete;
  void push(Widget w);
  Widget pop();
  Widget top();
  std::size_type size();
private:
  // elided data members may be different from the generic class
};
template <typename T>
void stack<T>::push(T t) {
  // elided details may be different from the generic class
}
// other member function definitions elided
stack<Widget> WdgStk;
// now compiles if we don't try to copy or assign instances
// and exhibits correct operation
The C++ language guarantees that a specialization compiles in place of the generic class whenever the class is instantiated with a specialized type such as Widget
  1. Chapter 5 covers how incomplete designs happen and how to ensure designs are complete.

2.5 Libraries

The C++ standard library collection is very large - perhaps overwhelming. Most developers know a small subset well and browse the rest when they need functionality that might already be there.
standard C++ libraries The listing below enumerates many of the libraries and some of their contents to support browsing. This material comes from cppreference.com.
The organization has been adjusted slightly and a few obscure libraries omitted. Libraries used frequently are annotated and emphasized; most of them appear in examples throughout this story.
Here are some categories:
  1. Language Support libraries
    • <initializer_list> - supports uniform initialization for user-defined types, examples in Chapter #3 - Data.
    • <type_traits> - used for template metadata programming, examples in Chapter 3.
    • <limits> - Numeric limits
    • <cstdlib> - Managing OS Processes and Signals
  2. General Utilities libraries
    • <memory> - Smart pointers and allocators, examples in Chapter 4
    • <chrono> - Time durations, times, and dates
    • Function objects and qualilfiers:
      <functional> - function, mem_fn, bind, invoke, ref
      <utility> - move, forward, pair, tuple, examples in Chapters 4 and 5
      <tuple> - examples in Chapters 2 and 3
    • <charconv> - to_chars, from_chars, chars_format
    • <optional> - return instance or empty
    • <any> - holds instance of almost any type, example in Chapter 3
    • <variant> - holds instances of any specified set of types
  3. <string> - std::basic_string --> std::string, std::wstring
  4. Containers libraries: <array>, <deque>, <list>, <map>, <multimap> <multiset> <queue>, <set>, <singleList>, <stack>, <string>, <unordered_map>, <unordered_multimap>, <unordered_multiset>, <unordered_set>, <vector>
  5. <algorithm> - Algorithms library Non-modifying sequence operations:
    all_of, any_of, none_of, for_each, for_each_n, count, count_if, mismatch, find, find_if, find_if_not, find_end, find_first_of, adjacent_find, search, search_n
    Modifying sequence operations:
    copy, copy_if, copy_n, copy_backward, move, move_backward, fill, fill_n, transform, generate, generate_n, remove, remove_if, remove_copy, remove_copy_if, replace, replace_if, replace_copy, replace_copy_if, swap, swap_ranges, iter_swap, reverse, reverse_copy, rotate, shift_left, shift_right, random_shuffle, shuffle, sample, unique, unique_copy
    Partitioning operations:
    is_partitioned, partition, partition_copy, stable_partition, partition_point
    Sorting operations:
    is_sorted, is_sorted_until, sort, partial_sort, stable_sort, nth_element
    Binary search on sorted ranges:
    lower_bound, upper_bound, binary_search, equal_range
    Other operations on sorted ranges:
    merge, inplace_merge
    Set operations on sorted ranges:
    includes, set_differences, set_intersection, set_symmetric_distance, set_union
    Heap operations:
    is_heap, is_heap_until, make_heap, push_heap, pop_heap, sort_heap
    Min/Max operations:
    max, max_element, min, min_element, minmax, minmax_element, clamp
    Comparison operations:
    equal, lexicographical_compare, lexicographical_compare_three_way
    Permutation operations:
    is_permutation, next_permutation, prev_permutation
    Numeric operations:
    iota, accumulate, inner_product, adjacent_difference, partial_sum, reduce, exclusive_scan, inclusive_scan, transform_reduce, transform_exclusive_scan, transform_inclusive_scan
  6. Numerics libraries
    • <cstdlib>, <cmath> - Common math functions
    • <cmath> - Special math functions
    • <numeric>, <cmath> - Numeric algorithms
    • <random>, <cstdlib> - Pseudo-random number generators
    • <cfenv> - Floating-point environment
    • <complex> - Complex numbers, operations
  7. Input/Output libraries:
    • Terminal I/O:
      <ios>, <streambuf>, <ostream>, <istream>, <iostream>
    • File I/O:
      <fstream>
    • String I/O:
      <sstream>
    • Synchronized I/O:
      <syncstream>
    • I/O manipulators:
      <iomanip>
  8. <regex> - Regular Expressions library
  9. Thread Support libraries:
    C++17:
    <thread>, <mutex>, <shared_mutex>, <condition_variable>, <future>, <atomic>
    C++20:
    <semiphore>, <latch>, <stop_token>
  10. <filesystem> - Filesystem library
  11. Error Handling libraries:
    <exception>, <stdexcept>, <cerrno>, <cassert> <system_error>
  12. Other libraries
    • Localizations
    • Iterators
    • Concepts (C++20)
    • Named Requirements (C++20)
    • Ranges (C++20)
Example:  Display container contents using std::vector, std::for_each, and lambda This example uses the STL vector container and for_each algorithm with a lambda fold:
  std::vector<int> test{ 
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 
  };  
  
  size_t N = 4;
  
  auto fold = [N](auto t) {
    static size_t count = UINT_MAX - 1;
    if (++count > N) {
      count = 1;
      std::cout << "\n  ";
    }
    std::cout << t << " ";
  };

  std::for_each(test.begin(), test.end(), fold);
        
with output:
  1 2 3 4
  5 6 7 8
  9 10 11 12
  13 14 15 16  
  17
The standard library collection is very large, but a few surprising omissions remain. The filesystem library did not appear until C++17, and the standard still has no support for:
  • Network programming and inter-process communication
  • Processing XML or JSON data formats
  • Building Graphical User Interfaces
Building these is not too difficult using platform APIs and third-party libraries, especially the extensive Boost library.
custom libraries
These libraries, written entirely in C++ with platform APIs as needed, fill gaps in the standard C++ library.
Library Description
FileSystem Provides interfaces used by many of the applications in code repositories in this site. It was developed using Windows and Linux platform APIs. I plan to turn this into a wrapper for the std::filesystem which provides a different set of interfaces, incompatible with the existing applications.
FileSystem.html
XmlDocument A fairly complete processing library for reading, parsing, building, and writing XML to and from strings and files. XmlDocument.html
CppCommWithFileXfer Supports asynchronous message-passing communication between multiple endpoints, using the Sockets library, below. CppCommWithFileXfer.html
Sockets A sockets class hierarchy that handles IP4 and IP6 protocols for stream-based sockets. The library has versions for both Windows and Linux. Sockets.html
CppParser A rule based parser suitable for analyzing C, C++, C#, and Java. Parsing Blog, CppParser Repository, CppLexicalScanner.html
This completes the survey of the C++ programming language. Each of these topics: Data, Operations, Classes, Templates, and Libraries gets expanded in the following chapters with discussion, code examples, and occasional videos.

2.6 Survey Epilogue

This chapter presented most of the key ideas in this story about the C++ programming language. Many details were omitted; the following chapters supply them. This view of the language is enough to build useful C++ programs. For requirements that exceed it, consult the following chapters.

2.7 Programming Exercises

  1. Write code that saves an array of strings where the size of the array is specified at run-time. Show how to access stored items and how to deallocate the storage.
  2. Write a lambda that accepts a std::string message and displays it on the console with a second line composed of '-' characters.
    If the lambda prepends the message with a newline, indents it two spaces, and makes the underline string two characters longer, with a one character indent, the result creates a nice title. Can you create the lambda so it also accepts an underline character which defaults to '-'?
  3. Develop a class that accepts an initializer_list of strings when constructed and save the elements of the list in a std::vector. Write a member function that adds an additional string to the list. Demonstrate this class in a main() where you supply a list of your friends. Then add two additional new friends.
  4. Generalize the friends class to accept a list of std::tuples where the tuples provide a bit more information about your friends. Can you make this work for types other than std::tuple, perhaps a struct with the same information. The intent here is that, after the first change, you can use more than one data type without changing your friends class.

2.8 References

cppreference.com
cplusplus.com/reference
C/C++ language and standard libraries reference - MSDN
cpppatterns.com
Posts on Fluent C++
C++ Idioms
C++ weekly videos
riptutorial - documentation provided by StackOverFlow
mycplus.com tutorials
cppnow 2018
Declarative Style in C++