CppStory Repo

Chapter #3 - C++ Data Types and Data

sizes, initialization, standard types

3.0 Data Prologue

This chapter focuses on data: types, type qualifiers, type conversions, data values, initialization, value conversions, and memory layout. Most code examples in this chapter have complete working demonstrations in CppStoryRepo. Download and run the code while studying this chapter.
Quick Starter Example - Easy Data In & Out with std::tuple
This example uses std::tuple, a simple data aggregator, introduced with C++11. It holds an arbitrary number of items, each with arbitrary types. We could have declared the tuple as shown in the output pane, below. In this example we simply initialize it with literal data items, as shown in the code pane, and allow template type deduction1 to determine types of our tuple's items. Code: Gathering Data with std::tuple std::tuple tp{ 1, 1.5, "two" }; /* easy in */ auto [ta, tb, tc] = tp; /* easy out */ displayType( tp, " <= std::tuple tp{ 1, 1.5, \"two\" }", false ); displayValues(ta, tb, tc); /* code only compiles with C++17 option */ Output class std::tuple<int,double,char const *> <= std::tuple tp { 1, 1.5, "two" } 1 1.5 two std::tuple holds a linear sequence of instances. The first code line initializes std::tuple tp with a brace initializer. The second line extracts each item with a structured binding. That declares one variable for each tuple item, with types deduced by the auto declaration using template-style type deduction. Our code has an int ta, a double tb, and a const char * tc. Note: structured binding only works for containers holding public data, as is the case for std::tuple, std::pair, and structs with public data members. Conclusion: C++ additions since C++11 make it easy to gather data into collections and extract it back into individual items.
  1. Template type deduction will be covered in Chapter6 - templates.
This chapter presents the first part of a two-part story about C language-like facilities in C++, excluding classes and templates. The second part covers operations, with the same exclusions. We will, of course, cover classes and templates in a "modern C++" part, starting with Chapter 4.

3.1 Types

C++ is a statically typed language. All data requires a type and name declared with a declaration statement. The language enforces a definition-first rule: variable use must occur after its definition, in compiler scan order. Type
  • A set of allowed values
  • Operations that may be applied to these values
The C++ language provides a collection of fundamental types. Fundamental Data Types
  • integral: byte, bool, int, char, char16_t, char32_t, wchar_t
  • floating point: float, double
  • derived types: array, pointer, reference
Library and user-defined types augment these with an open-ended collection: User defined types
  • struct S { ... }
  • class C { ... }
  • enum class E { ... }
  • alias: using [NewName] = [TypeName]

3.1.1 Type Qualifiers

The C++ language defines qualifiers that modify how an instance of a type may be used: const, constexpr, and volatile. It also defines qualifiers that affect storage of data: short, long, signed, unsigned, and extern.
const qualifier - const int i;
const The const qualifier affects what operations apply and what methods may be applied to a data item.
  1. const prepended on a type declaration const int ci; makes ci immutable
  2. function and method parameters void f(const std:string& s) prevent the user variable s from being changed in f
  3. const postpended to a method declaration and definition void X::f(int i) const { ... } prevents changes to an instance, x ε X, when invoked as x.f(i);
  4. const applied to pointers const X* pX = &x prevents x from being changed via pX, e.g., *pX = x2; will fail to compile X* const pX = &x prevents pX being assigned a new address
Code example: void f(const std::string& s) { if(s.size() > 0) { char first = s[0]; // ok // s[0] = 'z'; -- fails to compile since s is const } } Code example: X x{3}; const X* pX = &x; X x1 = *pX; // ok *pX = 2; // fails to compile
short and long Qualifiers - short int; short and long Qualifiers short and long affect amount of storage allocated and range of values a qualified data item may hold.
  1. short prepended on a type definition for int short int i; guarantees that sizeof(short int) <= sizeof(int);
  2. long prepended on a type definition for int or double long double d; guarantees that sizeof(double) <= sizeof(long double) <= sizeof(long long double)
Code example: short int iArrs[5]{ 1, 2, 3, 4, 5 }; // size of iArrs is 10 bytes int iArr[5]{ 1, 2, 3, 4, 5 }; // size of iArr is 20 bytes long int iArrl[5]{ 1, 2, 3, 4, 5 }; // size of iArrl is 20 bytes long long int iArrll[5]{ 1, 2, 3, 4, 5 } // size of iArrll is 40 bytes
signed and unsigned Qualifiers - unsigned int; signed and unsigned Qualifiers signed and unsigned affect the range of values an int may hold.
  1. signed (default) may be prepended on a type definition for int or char signed int i; acceptable values of i occupy a range symmetric around the value 0
  2. unsigned may be prepended on a type definition for int or char unsigned int j; acceptable values of j lie in a range bounded at the bottom with 0
Code example: signed int i = -25; // range is -2,147,483,648 to 2,147,483,647 unsigned int j = 15; // range is 0 to 4,294,967,295 unsigned int k = UINT_MAX; // UINT_MAX is defined in <limits> k + 1 == 0; // value roll-over, statement is true, no compiler warning
static Qualifier - static int;
static The static qualifier allocates storage in static memory.
  1. static prepended on a local type definition (in a function) static int i = 3; instructs the compiler to place storage for this named entity in static memory initialized to the value 3. The compiler initializes it exactly once, no later than the first function entry, and does not re-initialize on subsequent entries. The value persists across function calls, changing only when the function assigns to it.
  2. static prepended on a member variable of a class or struct places that variable in static memory, so every instance of the class or struct shares the same value.
extern Qualifier - extern int;
extern The extern qualifier disables allocation of storage.
  1. extern prepended on a type definition at namespace or global scope extern int i; provides a declaration, but instructs the compiler that another compilation unit (.cpp file) provides storage so it should not be provided in this compilation unit.
inline Qualifier - inline const int = 3;
inline The inline qualifier for global variables was introduced with C++17.
  1. inline prepended on a type definition in a header file inline const double pi = 3.14159; instructs the linker to provide storage only once, even if the header file is included in multiple cpp files used to build one image. This eliminates one source of multiple definition linker errors.
volatile qualifier - volatile int i;
volatile The qualifier volatile affects what compiler optimizers may do with a data storage. Note that, unlike Java and C#, volatile qualification does not affect flushing of the cache for reads and writes.
  1. volatile prepended on a type definition volatile int i; instructs compiler that variable i may be changed by an event external to the program and so should not be optimized to a compile-time constant.
constexpr qualifier - constexpr int N = 10; constexpr The constexpr qualifier makes the initialized value known at compile-time.
  1. constexpr prepended on a const integral type definition initialized with constant instance constexpr int N = 10; allows N to be used for compile time constructs like native arrays and templates. We will discuss constexpr with if selection in the template meta-programming chapter.
Code example: constexpr unsigned int N = 10; double dArr[N] { 0.5, 1.0, 1.5 }; // compiles because N and initializers are known at compile-time

3.1.2 Type Specifiers

Since C++11, the language provides special declarations, auto and decltype, that use type deduction to establish the type of an expression. One important use is declaring a lambda. The compiler constructs lambda types, and their names are not public. Auto is a type placeholder for that construction. The decltype specifier evaluates the type of an expression, which you can then use to declare a variable of that type.
auto and decltype specifiers - auto d{ 1.5 };
auto auto is not a declaration qualifier, it is a declaration.
  1. auto as a type definition auto r{ 3 }; auto s{ 1.5 } instructs the compiler to use type deduction to supply the type, so r is type int and s is type double.
  2. auto is frequently used to deduce the type of a function return value or as the type of a variable enumerated in a range-based for: auto r = f();
    for(auto item : container) { ... }
    auto type deduction is similar to the type deduction process used for templates.
Code example: std::vector<int> vecInt { 1, 2, 3 }; for(auto& item : vecInt) { item *= 2; } decltype Like auto, decltype is a declaration.
  1. decltype as a type definition decltype(make_lambda()) x; x is a lambda of the same type as created by the function make_lambda
Code example: decltype(vecInt) vecInt2; // results in std::vector<int> instance, named vecInt2, // with default initialization

3.2 Exploring Types

Occasionally we need to know the sizes of variables in our designs. For example, serializing an object into a byte array for socket transport requires knowing the object's size. Knowing type sizes also helps us understand how types work, as the lambda example below illustrates. The operator sizeof can be applied to a type or an expression: size_t sz = sizeof(arg); It returns the size of the arg parameter in bytes for all types except references. For references sizeof(X&) returns the size of the referenced type, not the size of the reference. The size of a reference equals the size of a pointer, which, on my work machine, is 4 bytes, e.g., 32 bits. You can verify this by constructing a class Xref with a single reference member and evaluating sizeof(Xref). The operator typeid can be applied to a type or an expression as: std::type_info ti = typeid(CT) where CT is any complete type. Using typeid requires including <typeinfo>. type_info instances like ti hold much less information than reflection results from C# or Java. However, type_info has useful methods: operator==(const type_info&) and operator!=(const type_info&) to query whether two instances belong to the same type, and name(), so we can discover the full type name of some instance.
Example: Using sizeof and typeid to explain how lambdas work
Here is a template function, useful for exploring types, that uses both sizeof and typeid:
 template<typename T>
  void displayType(const T& t, const std::string& msg = "", bool showSize = true)
  {
    std::cout << "\n  ";
    if (showSize)
      std::cout << sizeof(t) << " = size of ";
    std::string typeName = typeid(t).name();
    if (typeName.size() > 75)
      typeName = typeName.substr(0, 75) + "...";
    std::cout << typeName;
    if (msg.size() > 0)
      std::cout << msg;
  }
It provides one way to begin understanding how lambdas work:
  int i{ 42 };
  auto f = [i](const std::string& s) {  
    std::cout << s << i;
  };
  f("\n  The meaning of life is ");
  displayType(f);
Here, f is a lambda declared with auto because the lambda type is unknown. The [i] capture clause captures the value of i, which is 42. The (const std::string& s) parameter passes a string by const reference to the lambda body. The body { ... } displays the parameter and the captured integer. The output of this code is:
                  The meaning of life is 42
  4 = size of class <lambda_1bed100c18f4584a5d93f1a5d7e27280>
The displayType function reveals that the lambda is a class, discloses its type name (not intended for human consumption), and shows that its size equals the captured int value. The compiler most likely created a functor like this:
                  class lambda_1bed1... {
  public:
    void operator()(const std::string& s) {
      std::cout << s << i;    
    }
  private:
    int i = 42;
  };
Lambdas can capture data and be passed into or returned from functions, carrying their data with them (provided they hold values, not references). The lambda and its enclosing scope form a closure. A lambda can use any data in its closure. A lambda has two sets of inputs: from its capture list [i, j, x] drawn from the lambda's closure, and (T1 arg1, T2 arg2, ... ) passed in from the scope where the lambda is invoked. The lambda's designer provides the capture inputs and the lambda user provides the arguments.
In the next section, we continue exploring types by looking at their layout in memory.

3.3 Type Structure

Fig 1. String Layout Type structure is also worth examining. Fundamental types place all their value information in one location, the stack frame where they are declared. Not all types work that way. A std::string, for example, stores its control state, iterator, and character count in the stack frame where it is declared, but stores its character sequence in the heap. The sizeof operator measures only the stack allocation. So, for example, it will return the same size for an empty string and a string with 100 characters. For all of the STL sequential containers with contiguous heap memory, e.g., string, vector, and deque, you can call their capacity() method to measure that allocation. Here's an example: std::string str("content of a std::string"); std::cout << "\n " << str; std::cout << "\n string size = " << (str.size() + 1) * sizeof(char); std::cout << "\n allocation size = " << str.capacity() * sizeof(char); When this code executes you get: content of a std::string string size = 25 allocation size = 31 For other containers, measuring heap allocation requires more effort, but the approach is similar. Iterate through the container, measuring each element accessed by the container's iterator. Even the fundamental types differ in structure:
  1. All of the fundamental types except floats have a simple structure consisting of a sequence of bytes in one location.
  2. The float type partitions its storage into:
    • 1 bit sign
    • 8 bit exponent
    • 23 bit fraction
  3. The double type has the partitions:
    • 1 bit sign
    • 11 bit exponent
    • 52 bit fraction
This float/double structure has consequences:
  1. Most values don't have exact representation. Integers all have exact floating-point representations, but numbers like 1.0/3.0 have infinitely many trailing digits, e.g., 0.33333333333333...., and are stored imprecisely.
  2. So these values have granularity with differences within granules that can't be represented.
  3. The size of the granularity depends on the magnitude of the float. The fractional value of one float may be the same as the fractional value of another, but if the exponent of one is larger than the other, the absolute values of granularity will differ.
  4. Two conceptually distinct floats may share the same floating-point representation, so use caution with equality comparisons.
  5. Repeating operations on a float in a loop can produce increasing error in computed results due to accumulated granularity errors.
Here's data from an experiment with floats: --- demo float granularity --- float f = 1.00000 f = (f + 1.0)/3.0 ==> 0.666667 f = (f + 1.0)/3.0 ==> 0.555556 f = (f + 1.0)/3.0 ==> 0.518519 f = (f + 1.0)/3.0 ==> 0.506173 f = (f + 1.0)/3.0 ==> 0.502058 f = (f + 1.0)/3.0 ==> 0.500686 f = (f + 1.0)/3.0 ==> 0.500229 f = (f + 1.0)/3.0 ==> 0.500076 f = (f + 1.0)/3.0 ==> 0.500025 f = (f + 1.0)/3.0 ==> 0.500008 Reversing process: f = 3.0 * f - 1.0 ==> 0.500025 f = 3.0 * f - 1.0 ==> 0.500076 f = 3.0 * f - 1.0 ==> 0.500229 f = 3.0 * f - 1.0 ==> 0.500686 f = 3.0 * f - 1.0 ==> 0.502057 f = 3.0 * f - 1.0 ==> 0.506170 f = 3.0 * f - 1.0 ==> 0.518510 f = 3.0 * f - 1.0 ==> 0.555531 f = 3.0 * f - 1.0 ==> 0.666594 f = 3.0 * f - 1.0 ==> 0.999782 Float granularity matters in many domains:
  • Scientific computing, e.g., processing models for particles, gravity,
    super-conductors, gene expression, climate dynamics, ...
  • Processing financial models for derivative and currency trading
  • Graphics processing using projections and ray-tracing
  • Medical imaging
  • Radar and Sonar signal processing
  • Finite-element analysis for fluid flow
  • Navigation computing for aircraft and spacecraft
  • Autonomous vehicle control.
  • Neural network modeling
  • Marketing models
  • Control of machine tools used to create manufactured products
  • ....
During your career, you will likely work in domains where floating-point granularity matters.

3.4 Managing C++ Type System with Casts

Cast operations map an instance of one type into an instance of another type, or modify the type rules applied to a given instance. C++11 introduced four modern cast forms:
static_cast Syntax static_cast Symantics
T2 t2 = static_cast<T2>(t1); Convert t1 ε T1 to an instance of T2 usually by constructing a new T2 instance using data from t1.
Example: static_cast<T>
 /*--------------------------------------------------------------------------------------
  purpose of static_cast is to create a new instance of a destination type, based on
  data stored in the source type.
 --------------------------------------------------------------------------------------*/  

  int i = 1.75;  // i = 1, with compiler warning of loss of significance
  int j = static_cast<int>(1.75);  // j = 1, no compiler warning
const_cast Syntax const_cast Symantics
T& tRef = const_cast<T&>(t); Strip the const qualifier from a const type. The intent is to pass a const variable to platform API functions that cannot declare themselves const, but are known not to change the variable.
Example: const_cast<T>
 /*--------------------------------------------------------------------------------------
  purpose of const_cast is to allow passing const data to functions that won't change
  value even though not declared as const functions, OS API calls for example.
 --------------------------------------------------------------------------------------*/   

  void mockAPIfunction(std::string* pStr) {
    std::cout << "\n  inside mock API function: " << *pStr;
  }

  void demoConstCast(const std::string& str) {

    displaySubtitle("const_cast");

    std::cout << "\n  " << str;
    //mockAPIfunction(&str);  fails to compile since str is const

    /*--- useful operation using sRef ---*/

    std::string& sRef = const_cast<std::string&>(str);
    // created non-const reference to const str

    mockAPIfunction(&sRef);  // succeeds since sRef is not const

    /*--- evil operation on sRef, violates contract of function interface ---*/
    /* don't do this */
    sRef = "changed";
    std::cout << "\n  " << str;  // now has changed value
    std::cout << "\n";
  }
  const_cast
 ------------
  const string
  inside mock API function: const string   
  changed
The compiler allows modifying the const_cast source, but you should never do that.
dynamic_cast Syntax dynamic_cast Symantics
Derived* pDer = dynamic_cast<Derived*>(pBase)
if(pDer != nullptr) {
  pDer->Derived::memFun();
}
The intent is to call a Derived member function not in the Base interface, starting from a Base pointer. If the Base pointer points to an instance of the requested type, the cast returns the pBase address typed as a Derived pointer; otherwise it returns nullptr.
Example: dynamic_cast<T>
 /*--------------------------------------------------------------------------------------
  dynamic_cast grants access to derived class interfaces starting with base pointer
 --------------------------------------------------------------------------------------*/   

  class Base {
  public:
    virtual ~Base() {}
    virtual void say() {
      std::cout << "\n  hello from Base::say() via " << typeid(*this).name();
    }
  };

  class Derived1 : public Base {
  public:
    virtual ~Derived1() {}
    void say1() {
      std::cout << "\n  hello from Derived1::say1() via " << typeid(*this).name();
    }
  };

  class Derived2 : public Base {
  public:
    virtual ~Derived2() {}
    void say2() {
      std::cout << "\n  hello from Derived2::say2() via " << typeid(*this).name();
    }
  };

  auto putline = [](int n=1) { 
    for(int i=0; i<n; ++i)
      std::cout << std::endl; 
  };

  void demoDynamicCast() {

    displaySubtitle("dynamic_cast");

    std::cout << "\n --- calls from objects ---\n";
    Base b; b.say();
    putline();
    Derived1 d1; d1.say(); d1.say1();
    putline();
    Derived2 d2; d2.say(); d2.say2();
    putline();

    std::cout << "\n --- call via base pointer ---\n";
    Base* pBase = &d1; pBase->say(); // pBase->say1(); not accessible from B*
    putline();

    std::cout << "\n --- call via dynamic_cast derived pointer ---\n";

    Derived1* pDer1 = dynamic_cast<Derived1*>(pBase);
    if (pDer1) {
      pDer1->say1();
    }
    putline();
  }
  dynamic_cast
 --------------

 --- calls from objects ---
  hello from Base::say() via class Base

  hello from Base::say() via class Derived1
  hello from Derived1::say1() via class Derived1

  hello from Base::say() via class Derived2
  hello from Derived2::say2() via class Derived2    

 --- call via base pointer ---
  hello from Base::say() via class Derived1

 --- call via dynamic_cast derived pointer ---
  hello from Derived1::say1() via class Derived1
          
reinterpret_cast Syntax
reinterpret_cast Symantics
Tdst* pTdst = reinterpret_cast<Tdst*>(pTsrc); Apply type rules of Tdst to tsrc ε Tsrc.
Usually used for packing data into byte arrays and unpacking back to original type.
Example: reinterpret_cast<T>
 /*--------------------------------------------------------------------------------------
  purpose of reinterpret_cast is to apply new type rules to an existing instance.
  - packing double's bytes into byte array
  - unpacking byte array into another double
  - illustrates how data might be marshalled over a socket channel, where the
    byte array pretends to be the socket channel
  --------------------------------------------------------------------------------------*/    

  void demoReinterpretCast() {

    displaySubtitle("reinterpret_cast");

    double d1{ 3.5 };
    double d2;
    size_t Max = sizeof(d1);

    /* create byte array on heap referenced by std::unique_ptr<std::byte> */

    std::unique_ptr<std::byte> pBuffer(new std::byte[Max]);  // owning pointer
    std::byte* pBuffIndex = pBuffer.get();                   // non-owning pointer

    /* pack double d1 into byte array */

    std::byte* pByteSrc = reinterpret_cast<std::byte*>(&d1);
    std::byte* pSrcIndex = pByteSrc;  			   // non-owning pointers

    for (size_t i = 0; i < Max; ++i) {
      *pBuffIndex++ = *pSrcIndex++;
    }

    /* unpack byte array into double d2 */

    if (sizeof(d2) == sizeof(d1)) {
      std::byte* pByteDst = reinterpret_cast<std::byte*>(&d2);
      std::byte* pDstIndex = pByteDst;  			   // non-owning pointers
      pBuffIndex = pBuffer.get();
      for (size_t i = 0; i < Max; ++i) {
        *pDstIndex++ = *pBuffIndex++;
      }
    }

    /* show that src and dst have the same values */

    std::cout << "\n  src double = " << d1;
    std::cout << "\n  dst double = " << d2;

    // byte array on heap will be deallocated here
    // as std::unique_ptr goes out of scope
  }
              
    reinterpret_cast
   ------------------     
    src double = 3.5
    dst double = 3.5
            
C++ also defines a member function cast operator that supports creating an instance of a specified destination type from an instance of the casting class: C++ cast operator class X {
public:
  operator Y () { ... }
};
which supports expressions like: X x;
Y y = x;
If we declared the cast as explicit: class X {
public:
  explicit operator Y () { ... }
};
then we would have to explicitly use static_cast: X x;
Y y = static_cast<Y>(x);

3.5 New Standard Types

std::pair has been part of standard C++ libraries since before C++11. The remaining containers were introduced with C++11 or later. std::pair and std::tuple appear in the examples below. The rest appear in sample code that accompanies this chapter. 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
These new standard types are not essential, but are effective friction reducers. They make routine code easier to produce and understand. Examples appear throughout the remainder of this story.

3.6 Initialization

This chapter has already touched on data initialization. This section examines it more closely. Fundamental types support four syntax forms:
  1. T t = u
  2. T t(u);
  3. T t = { u };
  4. T t{ u };
Where u ε U is some type coercible to T or promoted through a T(U u) constructor. The first two forms are equivalent, both invoking T(U u). The first form performs no assignment - its syntax is equivalent to the second. The third and fourth are equivalent to each other, but not to the first two. Narrowing conversions like: int i = 1.75; // size of int is 4 bytes, size of double is 8 bytes succeed with either of the first two, with a compiler warning. Braced initialization does not allow this: int j{ 1.75 }; // fails to compile Braced initialization, like case 4, applies to most data definitions and is called "uniform initialization".
Examples of "uniform initialization"
std::string s4{ "four" };
double pi{ 3.1415729 };
double* pDouble{ &pi };
double& rDouble{ pi };
std::pair<int, double> pair1{ 4, 4.5 };
std::vector<std::pair<int, double>> pVec{ pair1, pair2, pair3, pair4 };  
std::tuple<int, double, std::string> aTuple{ 1, 3.1415927, "some string" };
std::unordered_map<std::string, int> umap{ { "two", 2}, {"three", 3} };
std::unique_ptr<double> uPtr1{ std::make_unique<double>(pi) };
std::shared_ptr<double> sPtr1{ std::make_shared<double>(pi) };
Uniform initialization makes inserting data into containers easy. Since C++17, extracting data with structured bindings is equally easy.
Examples of "structured binding"
auto [i5, i6] = std::pair{ 5, 6 };
auto [d, i, s] = std::tuple<double, int, std::string>{ 2.5, 3, "four" };
struct S { int i; char c; std::string s; };
S foobar{ 2, 'Z', "a string" };
auto [fee, fie, foe] = foobar;
The identifiers i5, i6, d, i, s, fee, fie, foe are all instances with types deduced by auto, based on the types in the containers to which they bind. Use them like any other identifier after the binding statement.
These are declarations, so the identifier names must differ from any used before the binding statement in the same scope.
Note: Structured binding only works for public data in the source entity.
Example of where "structured binding" does not work.
class C {
public:
  C(const std::string& str) : str_(str) {} // promotion ctor  
  void say() { std::cout << "\n " << str_; }
  operator std::string() { return str_; } // cast operator
private:
  std::string str_;
};
/* uniform initialization works with promotion ctors */
C c{ "hello world" };
/* these fail to compile: can only bind to public data */
//auto [gotit] {c};
//auto [gotit] = { static_cast (c) };
auto gotit = c; // this works via the cast operator, but is not structured binding

3.7 Managing Allocation with Smart Pointers

C++ provides a large collection of Standard Template Library containers that manage allocations effectively. These are the first choice for managing data on the native heap1. When building data managers and factory functions, prefer smart pointers - std::unique_ptr and std::shared_ptr - over raw pointers. Smart pointers provide two benefits:
  1. No memory leaks, because smart pointers delete their allocations when they go out of scope.
  2. Users of factory functions that return smart pointers need not manage allocations - the smart pointers handle that.
The demonstration below illustrates both smart pointer use and the Dependency Inversion Principle. Factory functions return std::unique_ptr<IWidget>, providing access to Widget instances on the heap. The user accesses objects only through the IWidget interface, so she has no dependency on Widget implementation details. Using a factory means she has no dependency on construction details, fully isolating her from the Widget implementation.
Smart Pointers Chap2SmartPtrs.cpp #include <memory> #include <string> #include <iostream> #include "../Chapter7-Display/Chap7Display.h" std::string displayHelper( const std::string& str ) { std::string insert = (str.size() > 0 ? " " + str + " " : " "); return insert; } /*-- interface for Widget class --*/ struct IWidget { virtual ~IWidget() {} virtual void say() = 0; virtual std::string name() = 0; virtual void name(const std::string& nm) = 0; }; /*-- Widget class --*/ class Widget : public IWidget { public: Widget() = default; Widget(const std::string& nm) { name_ = nm; } ~Widget() { std::cout << "\n destroying " << displayHelper(name_); } virtual void say() override { std::cout << "\n Widget instance" << displayHelper(name_) << "here"; std::cout << "\n my address is " << reinterpret_cast<long long>(this); } virtual std::string name() override { return name_; } virtual void name(const std::string& nm) { name_ = nm; } private: std::string name_; }; Smart Pointer Factories: /*------------------------------------------- first factory function uses unique_ptr */ std::unique_ptr<IWidget> createWidget1() { return std::unique_ptr<Widget>(new Widget); } /*-------------------------------------------- second factory function uses make_unique */ std::unique_ptr<IWidget> createWidget2() { return std::make_unique<Widget>(); } /*------------------------------------------- third factory function uses initialized make_unique */ std::unique_ptr<IWidget> createWidget3(const std::string& name) { return std::make_unique<Widget>( Widget(name) ); } /*------------------------------------------- fourth factory function uses static Widget for make_unique avoiding repeated constr. */ std::unique_ptr<IWidget> createWidget4(const std::string& name) { static Widget widget; widget.name(name); return std::make_unique<Widget>( widget ); } Using Code: int main() { displayTitle("Demonstrating Smart Pointers"); displayDemo( "--- createWidget1 with std::unique_ptr ---" ); std::unique_ptr<IWidget> pWidget = createWidget1(); pWidget->say(); pWidget->name("Joe"); pWidget->say(); putline(); displayDemo( "--- createWidget2 with std::make_unique ---" ); pWidget = createWidget2(); pWidget->name("Zhang"); pWidget->say(); putline(); displayDemo( "--- createWidget3 with initialized " "std::make_unique ---" ); pWidget = createWidget3("Ashok"); pWidget->say(); putline(); displayDemo( "--- createWidget4 with " "static initialized " "std::make_unique ---" ); pWidget = createWidget4("Priyaa"); pWidget->say(); putline(); displayDemo( "--- std::shared_ptr ---" ); std::shared_ptr<IWidget> pS1Widget = std::make_shared<Widget>(Widget("Mike")); pS1Widget->say(); std::shared_ptr<IWidget> pS2Widget = pS1Widget; pS2Widget->say(); putline(); displayDemo( "--- std::shared_ptr via factory---" ); std::shared_ptr<IWidget> pS3Widget = createWidget4("Sally"); pS3Widget->say(); std::shared_ptr<IWidget> pS4Widget = pS3Widget; pS4Widget->say(); putline(); } Output: Demonstrating Smart Pointers ============================== --- createWidget1 with std::unique_ptr --- Widget instance here my address is 2786941491616 Widget instance Joe here my address is 2786941491616 --- createWidget2 with std::make_unique --- destroying Joe Widget instance Zhang here my address is 2786941490384 --- createWidget3 with initialized std::make_unique --- destroying Ashok destroying Zhang Widget instance Ashok here my address is 2786941490272 --- createWidget4 with static initialized std::make_unique --- destroying Ashok Widget instance Priyaa here my address is 2786941491504 --- std::shared_ptr --- destroying Mike Widget instance Mike here my address is 2786941451616 Widget instance Mike here my address is 2786941451616 --- std::shared_ptr via factory--- Widget instance Sally here my address is 2786941491280 Widget instance Sally here my address is 2786941491280 destroying Sally destroying Mike destroying Priyaa destroying Sally

  1. All of the STL containers manage data with heap allocations except for the std::array. That class uses stack allocation for a fixed number of data items.

3.8 Building Data Structures with STL Containers

This section shows how easy it is to compose STL containers into complex, useful data structures. STL details are deferred to Chapter 6.
This example saves file and path information from a directory tree, starting with a specified root path. The goal is to store each distinct filename and each distinct path exactly once, using several STL container classes.
A file name may appear in more than one directory. Rather than saving multiple path names, we save multiple iterators - much smaller than path names - into a path set.
The using declarations below show how these connect. The STL structure, using iterators to point into contents, works well. The complete declaration of the file information container is: std::map<std::string,std::vector<std::set<std::string>::iterator>>  --> std::set<std::string> The using declarations make that much more readable: FileInfo<File, PathRefs> --> PathSet<Path> The class diagram and declaration are shown in the blocks, below.
Fig 1. FileInfo Data Structure class FileInfoContainer { public: // file to path structure using File = std::string; using Path = std::string; using PathSet = std::set<Path>; using PathRef = PathSet::iterator; using PathRefs = std::vector<PathRef>; using FileInfo = std::map<File, PathRefs>; using iterator = FileInfo::iterator; using PathInfo = FileInfo; FileInfoContainer& add(const File& file, const Path& path); FileInfoContainer invertFileInfo(FileInfoContainer* pFIC); FileInfo fileInfo(); iterator begin() { return fileInfo_.begin(); } iterator end() { return fileInfo_.end(); } size_t fileCount(); size_t pathCount(); private: FileInfo fileInfo_; PathSet pathSet_; PathInfo pathInfo_; };
The output below comes from a directory navigator that stores file and directory information in a FileInfo container. Two output collections appear. The one on the left uses the FileInfo container. The one on the right uses another FileInfo container to hold the inverted path and file relationships.
Instead of file names as keys and sets of path names as values, we swap the roles of file and path. The call fileInfoContainer.add(file, path) inverts to fileInfoContainer.add(path, file). Both outputs are useful. The left output shows all paths for each file name, which helps find the latest version or older files to discard.
The right output shows how files group into applications.
FileInfo from path ".."
-------------------------
CallableObjects.cpp
  C:\github\JimFawcett\CppStory\Chapter3-CallableObjects
Chap1.cpp
  C:\github\JimFawcett\CppStory\Chapter1
Chap1.h
  C:\github\JimFawcett\CppStory\Chapter1
Classes.cpp
  C:\github\JimFawcett\CppStory\Chapter4-classes
Coercion.cpp
  C:\github\JimFawcett\CppStory\Chapter3-Coercion
Cpp11-BlockingQueue.h
  C:\github\JimFawcett\CppStory\Chapter3-Logger
CustomTraits.h
  C:\github\JimFawcett\CppStory\CustomTraits
Data.cpp
  C:\github\JimFawcett\CppStory\Chapter2-Data
DateTime.cpp
  C:\github\JimFawcett\CppStory\Chapter2-STL
  C:\github\JimFawcett\CppStory\Chapter3-Logger
  C:\github\JimFawcett\CppStory\DirWalker
DateTime.h
  C:\github\JimFawcett\CppStory\Chapter2-STL
  C:\github\JimFawcett\CppStory\Chapter3-Logger
  C:\github\JimFawcett\CppStory\DirWalker
Dev.cpp
  C:\github\JimFawcett\CppStory\Chapter1-Dev
Dev.h
  C:\github\JimFawcett\CppStory\Chapter1-Dev
DirWalker.cpp
  C:\github\JimFawcett\CppStory\Chapter2-STL
  C:\github\JimFawcett\CppStory\DirWalker
DirWalker.h
  C:\github\JimFawcett\CppStory\Chapter2-STL
  C:\github\JimFawcett\CppStory\DirWalker
Display.h
  C:\github\JimFawcett\CppStory\Chapter3-Logger
  C:\github\JimFawcett\CppStory\Display
Functions.cpp
  C:\github\JimFawcett\CppStory\Chapter3-functions
Functions1.cpp.html
  C:\github\JimFawcett\CppStory\Chapter3-functions
IDev.h
  C:\github\JimFawcett\CppStory\Chapter1-Dev
IPerson - Copy.h
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson.h
  C:\github\JimFawcett\CppStory\Chapter1-Dev
  C:\github\JimFawcett\CppStory\Chapter1-Person
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson1.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson2.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson3.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson4.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson5.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson6.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson7.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson8.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
IPerson9.h.html
  C:\github\JimFawcett\CppStory\Webber\ToHTML
Init.cpp
  C:\github\JimFawcett\CppStory\Chapter2-Init
Logger.cpp
  C:\github\JimFawcett\CppStory\Chapter3-Logger
Logger.h
  C:\github\JimFawcett\CppStory\Chapter3-Logger
Overloading.cpp
  C:\github\JimFawcett\CppStory\Chapter1-overloading
Overriding.cpp
  C:\github\JimFawcett\CppStory\Chapter1-Overriding
Overriding.h
  C:\github\JimFawcett\CppStory\Chapter1-Overriding
Person.cpp
  C:\github\JimFawcett\CppStory\Chapter1-Dev
  C:\github\JimFawcett\CppStory\Chapter1-Person
  C:\github\JimFawcett\CppStory\Webber\ToHTML
Person.h
  C:\github\JimFawcett\CppStory\Chapter1-Dev
  C:\github\JimFawcett\CppStory\Chapter1-Person
  C:\github\JimFawcett\CppStory\Webber\ToHTML
PersonDisplay.h
  C:\github\JimFawcett\CppStory\Chapter1-Person
PersonTest.cpp
  C:\github\JimFawcett\CppStory\Chapter1-PersonTest
STL.cpp
  C:\github\JimFawcett\CppStory\Chapter3-STL
STL_DataStructures.cpp
  C:\github\JimFawcett\CppStory\Chapter2-STL
Sizes.cpp
  C:\github\JimFawcett\CppStory\Chapter2-sizes
Survey.cpp
  C:\github\JimFawcett\CppStory\Chapter1-Survey
TestDev.cpp
  C:\github\JimFawcett\CppStory\Chapter1-DevTest
TestPerson.cpp
  C:\github\JimFawcett\CppStory\Chapter4-classes
ToHTML.cpp
  C:\github\JimFawcett\CppStory\Webber\ToHTML
const_cast.cpp
  C:\github\JimFawcett\CppStory
const_cast1.cpp.html
  C:\github\JimFawcett\CppStory
const_cast1.txt.html
  C:\github\JimFawcett\CppStory
dynamic_cast.cpp
  C:\github\JimFawcett\CppStory
dynamic_cast1.cpp.html
  C:\github\JimFawcett\CppStory
dynamic_cast2.cpp.html
  C:\github\JimFawcett\CppStory
reinterpret_cast.cpp
  C:\github\JimFawcett\CppStory
reinterpret_cast1.cpp.html
  C:\github\JimFawcett\CppStory
static_cast.cpp
  C:\github\JimFawcett\CppStory
static_cast1.cpp.html
  C:\github\JimFawcett\CppStory

number of files: 56
number of paths: 23
PathInfo from path ".."
-------------------------
C:\github\JimFawcett\CppStory
  const_cast.cpp
  const_cast1.cpp.html
  const_cast1.txt.html
  dynamic_cast.cpp
  dynamic_cast1.cpp.html
  dynamic_cast2.cpp.html
  reinterpret_cast.cpp
  reinterpret_cast1.cpp.html
  static_cast.cpp
  static_cast1.cpp.html
C:\github\JimFawcett\CppStory\Chapter1
  Chap1.cpp
  Chap1.h
C:\github\JimFawcett\CppStory\Chapter1-Dev
  Dev.cpp
  Dev.h
  IDev.h
  IPerson.h
  Person.cpp
  Person.h
C:\github\JimFawcett\CppStory\Chapter1-DevTest
  TestDev.cpp
C:\github\JimFawcett\CppStory\Chapter1-Overriding
  Overriding.cpp
  Overriding.h
C:\github\JimFawcett\CppStory\Chapter1-Person
  IPerson.h
  Person.cpp
  Person.h
  PersonDisplay.h
C:\github\JimFawcett\CppStory\Chapter1-PersonTest
  PersonTest.cpp
C:\github\JimFawcett\CppStory\Chapter1-Survey
  Survey.cpp
C:\github\JimFawcett\CppStory\Chapter1-overloading
  Overloading.cpp
C:\github\JimFawcett\CppStory\Chapter2-Data
  Data.cpp
C:\github\JimFawcett\CppStory\Chapter2-Init
  Init.cpp
C:\github\JimFawcett\CppStory\Chapter2-STL
  DateTime.cpp
  DateTime.h
  DirWalker.cpp
  DirWalker.h
  STL_DataStructures.cpp
C:\github\JimFawcett\CppStory\Chapter2-sizes
  Sizes.cpp
C:\github\JimFawcett\CppStory\Chapter3-CallableObjects
  CallableObjects.cpp
C:\github\JimFawcett\CppStory\Chapter3-Coercion
  Coercion.cpp
C:\github\JimFawcett\CppStory\Chapter3-Logger
  Cpp11-BlockingQueue.h
  DateTime.cpp
  DateTime.h
  Display.h
  Logger.cpp
  Logger.h
C:\github\JimFawcett\CppStory\Chapter3-STL
  STL.cpp
C:\github\JimFawcett\CppStory\Chapter3-functions
  Functions.cpp
  Functions1.cpp.html
C:\github\JimFawcett\CppStory\Chapter4-classes
  Classes.cpp
  TestPerson.cpp
C:\github\JimFawcett\CppStory\CustomTraits
  CustomTraits.h
C:\github\JimFawcett\CppStory\DirWalker
  DateTime.cpp
  DateTime.h
  DirWalker.cpp
  DirWalker.h
C:\github\JimFawcett\CppStory\Display
  Display.h
C:\github\JimFawcett\CppStory\Webber\ToHTML
  IPerson - Copy.h
  IPerson.h
  IPerson1.h.html
  IPerson2.h.html
  IPerson3.h.html
  IPerson4.h.html
  IPerson5.h.html
  IPerson6.h.html
  IPerson7.h.html
  IPerson8.h.html
  IPerson9.h.html
  Person.cpp
  Person.h
  ToHTML.cpp

number of files: 56
number of paths: 23
Directory navigator DirWalk<App> uses C++17 std::filesystem. It gets access to a FileInfoContainer from the App template parameter, which supplies methods doDir(...) and doFile(...). Those methods add files with their paths into a FileInfoContainer member of the App class.
This example illustrates how useful the STL types are. Chapter 6 covers the details.

3.9 Data Epilogue

The data story has two parts: types and values. This chapter has explored the C++ type system and mechanisms to declare and initialize instances of those types. Like many programming languages, C++ employs a strict type system. Violating type rules almost always causes compile failure. Templates, covered later, manage types in a more nuanced way. Data, as covered in this story, encompasses much more than the fundamental types the language provides. Our definition of data includes instances of standard library types and user-defined types. This concludes Chapter 3. In Chapter 4 we will look at C++ operations: functions, methods, functors, lambdas, ...

3.10 Data - Programming Exercises

  1. Write code that declares two floating point numbers and initializes one to a known value. Write a function that, using pointers, copies each byte of the initialized number into the other. Verify that after the copy both numbers hold the same value. Write another function that compares the two numbers, byte by byte, using pointers.
    You will want to ensure that no buffer overruns occur, by using the sizeof operator.
  2. Write a function that accepts a string and returns a count of the number of words in the string. For this exercise assume that words always begin and end adjacent to whitespace characters excluding the beginning and end of the string.
  3. Expand on the last exercise by building a key-value pair collection where the keys are discovered words and the value is the number of times it occurs in the input string. You may wish to look at std::map<std::string, size_t>. Note that the first time you encounter a word your map will contain { word, 1 }. If you encounter that word again you don't try to add as a new key. Instead you increment the value, e.g., { word, 2 }.
  4. Extend this exercise one more time by creating a black-list of words not entered, e.g., articles and conjunctions: the, and, or, when, ... Look up each discovered word and enter into the map only if it is not on the black-list. That means you will look up every word, so you want to use a data structure that does look-ups efficiently.

3.11 References

Variables and Types - cplusplus.com
Built-in types (C++) - MSDN
Types - austincc.edu/akochis