about
10/18/2022
C++ Story Data
Chapter #3 - C++ Data Types and Data
sizes, initialization, standard types
3.0 Data Prologue
Quick Starter Example - Easy Data In & Out with std::tuple
- Template type deduction will be covered in Chapter6 - templates.
3.1 Types
- A set of allowed values
- Operations that may be applied to these values
- integral: byte, bool, int, char, char16_t, char32_t, wchar_t
- floating point: float, double
- derived types: array, pointer, reference
struct S { ... } class C { ... } enum class E { ... } - alias:
using [NewName] = [TypeName]
3.1.1 Type Qualifiers
const qualifier - const int i;
-
const prepended on a type declaration
const int ci; ci immutable -
function and method parameters
void f(const std:string& s) -
const postpended to a method declaration and definition
void X::f(int i) const { ... } x ε X , when invoked as x.f(i); -
const applied to pointers
const X* pX = &x X* const pX = &x
short and long Qualifiers - short int;
-
short prepended on a type definition for int
short int i; sizeof(short int) <= sizeof(int); -
long prepended on a type definition for int or double
long double d; sizeof(double) <= sizeof(long double) <= sizeof(long long double)
signed and unsigned Qualifiers - unsigned int;
-
signed (default) may be prepended on a type definition for int or char
signed int i; acceptable values of i occupy a range symetric around the value 0 -
unsigned may be prepended on a type definition for int or char
unsigned int j; acceptible values of j lie in a range bounded at the bottom with 0
static Qualifier - static int;
-
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.That means that it will be initialized exactly once, no later than the first time the function is entered. It will not be re-initialized again on subsequent entries so its value persists across function calls, only changing when the function's code assigns to it. - 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 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 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 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 prepended on a const integral type definition initialized with constant instance
constexpr int N = 10; we will discuss use of constexpr with an if selection when we discuss template meta programming.
3.1.2 Type Specifiers
Since C++11, the language has provided special declarations, auto and decltype
that use type deduction to establish the type of an expression. One important use is to provide a declaration for
a lambda. Lambda types are constructed by the compiler and the names of those types are
not public. Auto is a type placeholder for that construction.
The decltype specifier evaluates the type of an expression which then may be used to declare
a variable of that type.
auto and decltype specifiers - auto d{ 1.5 };
-
auto as a type definition
auto r{ 3 }; auto s{ 1.5 } r is type int ands is type double. -
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) { ... }
-
decltype as a type definition
decltype(make_lambda()) x;
3.2 Exploring Types
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 we don't know the lambda type.
the [i] part of the expression says that this lambda captures the value of i, e.g., 42.
The (const std::string& s) defines a string parameter, passed to the lambda code
by const reference. The body { ... } simply displays the parameter and the
captured integer value.
The output of this code is:
The meaning of life is 42 4 = size of class <lambda_1bed100c18f4584a5d93f1a5d7e27280>
So, the displayType function has informed us that the lambda is a class, it disclosed the
lambda's type name, obviously not intended for human consumption, and showed us that its
size is just the size of the captured int value.
It's highly likely that the compiler created a functor, something like this:
class lambda_1bed1... { public: void operator()(const std::string& s) { std::cout << s << i; } private: int i = 42; };
So lambdas can capture data, be passed into or returned from functions, taking their data
with them (as long as they hold a value, not a reference). We say that the lambda and its
local scope are a closure. A lambda can use any data in its closure as part of its
operation.
Essentially, a lambda has two sets of inputs: from its capture list [i, j, x]
which come from the lambda's closure, and (T1 arg1, T2 arg2, ... )
which are 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.
3.3 Type Structure
- All of the fundamental types except floats have a simple structure consisting of a sequence of bytes in one location.
-
The float type partitions its storage into:
- 1 bit sign
- 8 bit exponent
- 23 bit fraction
-
The double type has the partitions:
- 1 bit sign
- 11 bit exponent
- 52 bit fraction
- The majority of values don't have an exact representation. All of the intergers have exact floating representations, but numbers like 1.0/3.0 have an infinite number of trailing digits, e.g., 0.33333333333333...., and will be stored imprecisely.
- So these values have granularity with differences within granules that can't be represented.
- 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.
- It is entirely possible for two floats that are conceptually distinct to have the same floating representation, so you need to be careful with equality comparisons.
- Repeating operations on a float in a loop may exhibit increasing error in computed results due to the accumulation of granularity errors.
-
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
- ....
work in domains where floating point granularity matters.
3.4 Managing C++ Type System with Casts
static_cast Syntax | static_cast Symantics |
---|---|
Convert |
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 |
---|---|
|
Strip off const qualifier from a const type. Intent is to pass const variable to platform API functions
which can't inform compiler that they won't change variable, but designer knows they
won't. |
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
This illustrates that the compiler will let you change the source of the const_cast
but you should never do that.
dynamic_cast Syntax | dynamic_cast Symantics |
---|---|
pDer->Derived::memFun(); } |
Intent is to call, starting with |
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 |
---|---|
|
Apply type rules of 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
public:
operator Y () { ... }
};
Y y = x;
public:
explicit operator Y () { ... }
};
Y y = static_cast<Y>(x);
3.5 New Standard Types
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 |
3.6 Initialization
- T t = u
- T t(u);
- T t = { u };
- T t{ u };
Examples of "uniform initialization"
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) };
Examples of "structured binding"
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;
Note that these are declarations, so the identifier names must be different than
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.
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
3.7 Managing Allocation with Smart Pointers
- There are no memory leaks because smart pointers delete their allocations when they go out of scope.
- Users of creational funtions that return smart pointers can ignore allocation management. The smart pointers do that for them.
Smart Pointers
-
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
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 |
3.9 Data Epilogue
3.10 Data - Programming Exercises
-
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 intialized 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. - 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.
-
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 } . - Extend this exercise one more time by creating a black-list of words not entered, e.g., articals and conjuctions: 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
Built-in types (C++) - MSDN
Types - austincc.edu/akochis