about
10/17/2022
C++ Story Survey
C++ Story Code

Chapter #2 - C++ Survey

data, operations, classes, templates, libraries

2.0 Survey Prologue

This chapter provides a quick look at programming facilities provided by C++ and its libraries. You will find a lot more details, with examples, on these topics in subsequent chapters. It is challenging to discuss details of a programming language without using terms and ideas that haven't yet been introduced. The intent of this chapter is to give you most of the basic ideas so that in later chapters, when we use a language construct or idea that hasn't yet been discussed fully, you will be able to follow the discussion, and have some confidence that details of the idea will be presented eventually.

2.1 Data Types

The C++ programming language provides fundamental types, built into the language, additional types provided by the C++ standard libraries, and supports building user-defined types. Those can be designed to provide essentially the same behaviors as 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. We rarely write code like this outside a class that carefully manages allocation and deallocation. We will hear a lot more about this in subsequent chapters. One safe way to manage heap storage is with 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 will always be returned. If we didn't call release() the allocation would be returned when the unique_ptr went out of scope.
Example:  Reading command line arguments In this example, of arrays and pointers, command line arguments are displayed 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++ references are used 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 always defined in the reference declaration and cannot be changed. The value of xr is always the value of x, which can be changed. It is useful to think of a reference as another name for the referenced instance. References are most often used to pass arguments to a function by reference. Passing an argument to a function by value, f(T t), copies the argument's value to the function's stack frame. Passing a parameter by reference, f(T& t), simply creates a small reference in the function's stack frame, bound to the parameter in the caller's scope, instead of copying what could be a much larger object.
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 Example using std::vector<int> and for_each algorithm. We will use lambdas in this example, so you may wish to first peek at the lambda discussion at the end of the Operations section. 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> is designed to return a value computed in a function if available. If computation fails (a file open 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(); }
In the details dropdown, below, you will find a summary of 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 are often used 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 just like classes except that struct members are public by default, where class members are by default private. Often we use structs as in 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;
Usually, classes build program abstractions. That is, a class represents some domain entity like an order or product. They are also used to build implementation abstractions like blocking queues and thread pools. You will hear more about blocking queues and thread pools later in this story.
class C { ... };
class X { public: void set(const std::string& s); std::string get(); private: std::string str; };
A type alias simply provides another name for an existing type. Surprisingly they are very useful for making code readable. We might provide a type alias, fileName, for the std::string type. That helps readers of the code understand that values for this string are the names of files. We can use filename any place that the language accepts a std::string, e.g., as a function parameter type.
type alias: using AliasName = Sometype;
Aliases are designed to provide application specific domain names for standard types and to 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;
The ability to test types at compile time allows us to build very flexible template functions and methods. You will hear much more about that below, and especially in Chapter #7 - Templates. Run-time type testing is less often used, but allows us to display the name of the type of some variable and test whether two instances have the same type.
Testing types at compile-time and run-time Compile-time tests use template meta programming. That will be discussed in detail in Chapter 8. This is here because you will see it used before we get to 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.
}
In the next chapter we will use std::type_info to display information about types in several examples.

2.1.2 Definitions and Declarations

A type declaration associates a variable name with a type.
type declaration A type declaration defines a name associated with a specified type, e.g., a set of operations supported for the named entity, but defines no storage unless it is also a definition. If not, that must be done later, but before the entity is used. extern int x; The entity x supports all of the operations for ints, but no storage is defined by this statement. void f(int i); Declares f to be a function, to be defined later, accepting a single integer argument and returning nothing. class X; Declares X to be a class to be defined later. X is said to be an incomplete type and the statement is a forward declaration of X. class Y { /* operations elided */ }; We often say that class Y is defined by the statement above, but technically it is a declaration, because this statement defines the size required for instances of Y but no storage is allocated.
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 can specify the location of that storage, e.g., in static, stack, or heap memory. int x; int y{ 2 }; The variable x is declared and storage is allocated in the local stack frame, and filled with a default value;
The variable y is declared and storage is allocated in the local stack frame and 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 is compiled and allocated to static memory adjacent to other code from the surrounding program.
Life-time of this allocation is the life time of the program after the program's process has been created until the process begins termination.
X x; Y y(...); Reserves storage for x ε X in the stackframe for its local scope and initializes that storage with a default value.
Reserves storage in the local stackframe for y ε Y and initializes the storage using a Y constructor function that accepts the type(s) specified by the ... elipsis, 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 un-named instance of X. The storage for pX is initialized with the heap address of the X instance, and the storage of the instance of X is initialized by X's default constructor.
Life-time for this storage extends from the time the new operator is invoked until delete operator is called on pX.
We will provide examples of, and cover many details about, C++ data types in Chapter #3 - Data.

2.2 Operations

C++ programs consist of an ordered sequence of statements where a statement is an expression followed by a semicolon ";". Expressions may be declarative, operational, iterative, selection, and try-block expression types, and may be combinations of these. Declarative statements are compile-time artifacts, disappearing after compilation. All the others 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 execution 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.
We will discuss all of these run-time operations in this section, below.
C++ provides unary and binary operations for the fundamental types. It also provides operator functions that may be overloaded for user-defined types. Some of the most used operations and operators are illustrated here for both fundamental and user-defined types:
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.  
Users can define invocable functions at namespace scope, including the global namespace, but cannot define functions in function scope, e.g., no inner functions. Users define methods (functions bound to a specific class) in class scope.
Lambdas are locally defined callable objects that are 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 may be defined in namespace, function, and method scopes, and so, can serve the role of inner functions. They are useful for much more, as we will see in Chapter #4 - Operations.

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, we choose to make some members public with a "public:" declaration and others private with "private:".
By convention, we use structs to aggregate data, much like std::tuple. We also use them to define code interfaces, as described in Chapter 4. We use classes to implement some abstraction defined by its public members. If a class is well designed, only its methods have direct access to data it manages. That allows a class to make strong guarantees about the validity of its data.
Object Oriented Design consists of using classes and class relationships to structure program activities. Basic classes can be quite simple, defining only methods for managing and accessing class state, as discussed here. The language, however, provides additional design facilities for supporting value or reference type behavior. We will address those details in Chapter #5 - Classes.
basic class
Class Person declares personal data contained by each instance. These "stats" contain name, occupation, and age, held in a std::tuple.
Instances of the Person class can be copied and assigned because its only data member, personStats is a std::tuple. The std::tuple and its elements all have correct copy, assignment, and destruction semantics.
Compiler generated methods for copying, assignment, and destruction do those operations on each of a class's base classes and composed member data items. In this case, those generated operations simply use the 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;
};
                          
Note that Person defines four type aliases with names that make the class's code readable, easer to test, and easer to use.
It provides getter and setter methods for attributes occupation and age that should be changeable, and only a getter for the name attribute which should not.
References: CppStory Repository
Fig 1. Person Class code layout
If you look back at the Person class, in basic class - the previous details, you will see two things that might seem peculiar. There are two functions with the same name, Person, and they have no return values. C++ classes define constructors for building initialized instances of the class. The language recognizes constructors as any function with the same name as the class and no return values.
The fact that constructors return no values - not even void - is specified by the language. In order to handle two or more functions with the same name, C++ defined function overloading.
function overloading
Function overloading is the definition of 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 has an sts argument, used to initialize an internal data member. The first has no argument, so the internal data member has a default initialization.
C++ classes need the ability to initialize instances in different ways, like this, and since the class's constructors all have the same name, the compiler has to distinguish between them in some way.
It does that by treating a function name concatenated with the types of its arguments2 as the compiler's name for that function. This process of making qualified names is called name-mangling. That is what enables function overloading.
Function overloading is used in most class declarations, but a designer is free to use 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";
}
There are five relationships between C++ classes: inheritance, composition, aggregation, using, and friendship. The first four of these are supported by most object oriented languages.
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 that a person might assume. A class hierarchy like this could be used to build a Project Management Tool that tracked progress on a project 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, support decoupling the hierarchy from its users, so that changes to any of these classes don't require clients to change, as long as the interface doesn't change.
The abstract class SW-Eng provides shared code and types for all its derived classes.
The concrete classes Dev, TeamLead, and ProjMgr, represent roles that a Software Engineer may assume while working on the project. The Person class is the key abstraction. All of the other parts serve to endow a person with one or more roles, e.g., developer, team lead, or project manager.
All of the relationships between these classes are based on inheritance.
The ProjMgr class aggregates a Project instance, allowing a manager to move to another project when the current project completes.
The Project class composes a Budget since that is an integral part of the project. It aggregates Documents and code Baseline, as these don't exist at the beginning of the project.
Developers and Team Leads use the Baseline by contributing additions, but they don't have an ownership relationship - only the Project Manager can authorise deletions and creation of major new parts.
Inheritance supports sharing base class code with all of its derived classes. While this is useful, inheritance's most important feature is support for substituting derived class pointers and references wherever a function accepts a base class pointer or reference.
function overriding
Suppose that, in the SW-Eng abstract class, from the previous "class relationships" details, we've defined a virtual void doWork() method.
One might expect that Devs, TeamLeads, and PrjMgrs would each have their own work behaviors, and so, each should have its own definition for what doWork means. Virtual function overriding allows us to specify those different behaviors.
Virtual function overriding is the process of, in each derived class, providing a definition for the function execution of that function based on the type of the derived class, e.g., Dev, TeamLead, or PrjMgr. The virtual functions in each derived class must have the same signature, e.g., void doWork(), and this redefinition only applys to functions that are qualified as 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
We will discuss the details of techniques used in this example in Chapter 5.
Most C++ projects use classes extensively. Usually almost all of the code is devoted to implementing classes and using class instances. We will look at, and dissect, examples in Chapter 5.

2.4 Templates and Specialization

C++ templates provide a way of defining library functions and classes that depend on some unspecified type or types. STL containers like std::vector<T> are examples.
Template functions and classes need to be instantiated by using applications with specific type(s) before they can be used, e.g., std::vector<std::string>.
template functions
The template function max<T, T> accepts two arguments of the same type and returns an instance of that type with the value of the larger of the arguments. template <typename T>
T max(T t1, T t2) {
    return t1 > t2 ? t1 : t2;
}
This function, when instantiated with a specific type, will compile successfully provided that T has defined operator>(T& t). Otherwise, compilation fails.
There is another subtle problem with max<T, T>. It works correctly for fundamental types and instances of classes like std::string. However, if we make the following invocation:
const char* pStr = max<"aardvark", "zebra">;
the function compiles, but we may not get a result we expect. max<const char*, const char*> returns the larger of its two arguments, which, in this case are pointers, so we will get a pointer to whichever string is stored in the higher memory location, having nothing to do with lexicographic ordering of its arguments.
We see how to fix it by overloading max<T, T> for that specific case, 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 that the most specific function overload will be applied to compile a function. In the case of const char*, pStr is more specific than the template type T, so the second version will be compiled and the result will return the string with largest lexicographic order because that is what the C function strcmp returns.
This overloading guarantee is very important. It means that we can define a generic template for a class and then supply specific overloads for any types that are problematic.
This isn't a perfect solution, because there may be an open-ended set of types that cause problems, but if we encounter more problematic types, we can provide additional overloads.
template classes
C++ classes can be declared as templates too. 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 we would have to declare a stack class for each of these types. With templates the compiler does all that extra work for us.
For each instantiation, the compiler generates a class for that type. Syntactically, each of the above declarations defines a distinct class. That is, stack<int> is not the same type as stack<std::pair<std::string, int>> prStk;
template class specialization
Suppose that, for the user-defined type Widget, some stack<T> methods fail to compile or process a Widget incorrectly. That could happen if the stack provides copy and assignment operations, but Widget instances are not copyable or assignable, or those operations are incorrect for Widget due to an incomplete design1.
For that, we can use template specialization to fix that problem, in much the same way as we did by overloading max<T, T> in the discussion of overloading template functions. For the Widget specialization we would declare the stack<Widget> copy and assignment operations as 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 will be compiled instead of the generic class whenever the class is instantiated with a specialized type, like Widget
  1. We will discuss, in Chapter 5., how incomplete designs happen and how to ensure your designs are complete.

2.5 Libraries

The C++ standard libraries collection is very large, perhaps overwhelming. Many of us are quite familiar with a small subset, and browse through the rest when we need some functionality that could be there.
standard C++ libraries I've enumerated here a lot of the libraries and some of their contents in order to support your browsing process. This material was drawn from resources provided by cppreference.com.
Only the organization has been changed (slightly) and a few of the more obscure libraries omitted. I've annotated the material and emphasized those libraries I use frequently. You will see most of them 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 Here's an example using the STL vector container and for_each algorithm with 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 still there are a few surprising omissions. Until C++17 there was no filesystem library and there still is no support for:
  • Network programming and inter-process communication
  • Processing XML or JSON data formats
  • Building Graphical User Interfaces
However, building these is not too difficult using platform APIs and/or third party libraries, especially the extensive Boost library.
custom libraries
Libraries, written entirely in C++, using platform APIs as necessary, that fill in some omissions from 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 our survey of the C++ programming language. We will be expanding on each of these topics: Data, Operations, Classes, Templates, and Libraries in the following chapters of this story, with lots of discussion and code examples, and a few videos here and there.

2.6 Survey Epilogue

This chapter has presented most of the ideas in this story about the C++ programming language. We've left out a lot of details, but those will come in the following chapters. This simple view of the language is all you need to build useful C++ programs. If you encounter requirements that can't be implemented with them then look into 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++
  Next Prev Pages Sections About Keys