The C++ Programming Language – Part 10

This post will be mostly for personal reference as I go through Bjarne Stroustrop’s “The C++ Programming Language” 4th edition textbook. Some of the notes will appear random.

Chapter 8 – Structures, Unions, and Enumerations

A struct is a class where all members are public by default. A struct can have member functions (constructors, getters, setters) like a class. Use struct constructors to establish invariants. Recall that an invariant is something that must always hold true during the life of an object. For structs, sometimes it can be useful to establish the invariant that all members are initialized when a struct instance is created. C++ structs support the concept of fields, also referred to as bit-fields. It is not possible to take the memory address of a field and it is possible to have unnamed fields for padding purposes. Members are defined as a field by giving the bit width:

#include <iostream>

using namespace std;

struct BitFieldSample {

    /* 1 bit field indicating wind shear */
    bool wind_shear : 1;

    /* 3 bit field to indicate weather type */
    unsigned int weather_type : 3;

    /* 57 bit flag field for various flags */
    unsigned long int operational_flags : 57;

    /* unnamed 1 bit field to 8-byte align the struct in memory */
    unsigned int : 3;

};

int main()
{
    BitFieldSample bfs;

    cout << "BitFieldSample is " << sizeof(bfs) << " bytes.\n" << endl;

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_8$ ./bitfield 
BitFieldSample is 8 bytes.

Ironically, using small bit fields vs typically used built-in data types can sometimes increase the size of your program due to the code needed to operate on the bitfields being larger than built-in type manipulation code. Cool fact: “A bit-field with size zero has a special meaning: Start in a ‘new allocation unit.’ The exact meaning is implementation defined, but usually it means that the following field starts at a word boundary.” A zero-width bitfield must be unnamed. A zero-width bit field can cause the next field to be aligned on the next container boundary where the container is the same size as the underlying type of the bit field.

A union is a struct which all members are allocated at the same address so that the union only takes up as much space as its largest member. Union can only hold one member value at a time. Lets go ahead and not use unions. According to Stroustrup, unions are an overused feature, are error prone, and don’t make much of a performance difference to warrant use.

An enumeration is a type that can hold a set of integer values specific by the user. To keep the enumerators local to the enum’s namespace, use an enum class instead. This way, multiple enums in the same project can have the same enumerators. The underlying type of an enumerator must be either signed or unsigned int.

enum class TrafficLight { red, green, yellow };
enum class Colors { red, green, yellow, blue, white, purple };

Chapter 9 – Statements

The majority of statements in C++ are fairly straight forward for most programmers to understand and make use of. Therefore, I won’t going through the simple statements or the well-known facts. Rather, here’s some excerpts from Bjarne’s closing remarks:

Don't declare a variable until you have a value to initialize it with.
Avoid do-statements
Avoid goto

Chapter 10 – Expressions

Similar to chapter 9, this will be short with only interesting facts. The order of evaluation of subexpressions within an expression is undefined. Specifically, you can’t assume that the expression is evaluated left to right. Even though this code will execute properly the vast majority of the time (probably since its so small), its behavior is still undefined.

#include <iostream>

using namespace std;


int f(int x)
{   
    cout << "in f()\n";
    return x * 2;
}


int g(int x)
{   
    cout << "in g()\n";
    return x * 3;
}


int main()
{

    /* undefined behavior */
    int x = f(2) + g(6);
    cout << "x: " << x << endl;

    int v[4];
    int i = 1;
    /* undefined behavior
     * could be either v[1] = 1 or v[2] = 1; */
    v[i] = i++;
    cout << "v[i]: " << v[i] << endl;

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_8$ g++ -std=c++11 -o order_of_exec order_of_exec.cpp 
user@ubuntu:~/cpp/part_2/chapter_8$ ./order_of_exec 
in f()
in g()
x: 22
v[i]: 1
user@ubuntu:~/cpp/part_2/chapter_8$ ./order_of_exec 
in f()
in g()
x: 22
v[i]: 1
user@ubuntu:~/cpp/part_2/chapter_8$ ./order_of_exec 
in f()
in g()
x: 22
v[i]: 1
user@ubuntu:~/cpp/part_2/chapter_8$ ./order_of_exec 
in f()
in g()
x: 22
v[i]: 1
user@ubuntu:~/cpp/part_2/chapter_8$ ./order_of_exec 
in f()
in g()
x: 22
v[i]: 1

Note that the && and || operators guarantee that their left-hand operator is evaluated before their right-hand operator. Operator precedence levels and associativity rules reflect the most common usage. Parenthesis should be used whenever a developer is unsure about operator precedence; parenthesis can guarantee correct order of operations if used correctly. That being said, if it comes to the point of using parenthesis, your code is not clean and should be rewritten (perhaps assign part of the expression to a variable).

Recall that a constexpr is an expression to be evaluated at compile time and that is was introduced in C++11. A class with a constexpr constructor is called a literal type. To be simple enough to be a constexpr, a constructor must have an empty body and all members must be initialized by potentially constant expressions. Constexpr essentially provides a “miniature compile-time functional programming language.” This stuff is complex and I think its best reserved for very specific scenarios where runtime and is absolutely critical. A good read about the benefits of using constexpr can be found here. Lets take a look at one the examples on that site showing the runtime changes of using constexpr to calculate a Fibonacci sequence. Note that the time complexity of recursive Fibonacci is O(2^n), so its a very expensive function:

#include<iostream>

using namespace std;

long int fib(int n)          
{
    return (n <= 1)? n : fib(n-1) + fib(n-2);
}

int main ()
{
    // value of res is computed at runtime.  
    long int res = fib(45);       
    cout << res;
    return 0;
}

Not using constexpr, so that fibonacci return value is calculated at runtime:

user@ubuntu:~/cpp/part_2/chapter_8$ time ./constexpr_performance 1134903170
real	0m16.523s
user	0m16.510s
sys	0m0.013s

Now using constexpr, so that fibonacci return value is calculated at compile time:

#include<iostream> 

using namespace std; 
  
constexpr long int fib(int n) 
{ 
    return (n <= 1)? n : fib(n-1) + fib(n-2); 
} 
  
int main () 
{ 
    // value of res is computed at compile time.  
    const long int res = fib(45); 
    cout << res; 
    return 0; 
}

Output:

user@ubuntu:~/cpp/part_2/chapter_8$ time ./constexpr_performance 1134903170
real	0m0.005s
user	0m0.000s
sys	0m0.005s

Obviously since the runtime complexity is so terrible, constexpr here is absolutely critical. You may be wondering as I did, how the hell can compile time make this much of a difference vs runtime? Doesn’t the computer still have to recursively calculate Fibonacci? Interestingly, the compiler is making the Fibonacci function an O(n) function by memoization. From Wikipedia: “A memoized function ‘remembers’ the results corresponding to some set of specific inputs. Subsequent calls with remembered inputs return the remembered result rather than recalculating it, thus eliminating the primary cost of a call with given parameters from all but the first call made to the function with those parameters.” We’ll hopefully explore memoization and other compiler optimizations in future posts.

Leave a Reply