The C++ Programming Language – Part 11

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 10 – Expressions (Continued)

The implicit conversions that preserve values are called promotions. Integers are promoted automatically during arithmetic operations, short ints will become ints. These promotions are designed to normalize the size of variables for arithmetic operations. Therefore, promotion to long or long long does not occur. As always, there are exceptions to the rules, and exceptions to the exceptions to the rule. This seems to be a pattern in C++. Anyway, the types char16_t, char32_t, wchar_t, and plain enums that are already larger than int will be promoted to long if necessary. The following promotion table describes the promotions in the order they would occur. For example “-> int, unsigned int” means that first promotion to int will be attempted if possible, otherwise promotion to unsigned int will occur.

char, signed char, unsigned char, short int, unsigned short int -> int, unsigned int.

char16_t, char32_t, wchar_t, plain enum -> int, unsigned int, long, unsigned long, unsigned long long.

bit-field -> int, unsigned int, not promoted if all bit values don't fit into sizeof(int).

bool -> int

“The fundamental types can be implicitly converted into each other in a bewildering number of ways. In my opinion, too many conversions are allowed.” Some of the most dangerous conversions are called narrowing conversions. These conversions truncate types, resulting in loss of data or precision. C++ offers protection against narrowing conversions via narrow_cast<>(), a runtime checked conversion function. Another way to guard against narrowing is to utilize the {} initializer syntax. Using brackets prevents narrowing and will throw a compiler error, like “double precision floating point to char conversion.” Conversions are complex and the behavior should be evaluated on a case by case basis, since there are so many possibilities. When writing production code, always be wary of conversions and use a debugger to ensure that your data isn’t being silently clobbered. Here is a real-world example of an integer narrowing bug in Firefox that results in out-of-bounds array access (essentially allows an attacker to possibly leak process memory): link.

“Inside the JavaScript parser, a cast of an integer to a narrower type can result in data read from outside the buffer being parsed. This usually results in a non-exploitable crash, but can leak a limited amount of information from memory if it matches JavaScript identifier syntax. This vulnerability affects Firefox < 56.”

Pointer, integral, and floating-point values can be implicitly converted to bool. A nonzero value converts to true; a zero value converts to false. This can allow for some dubious development practices, like the following code. Note that Bjarne states “Hope for a compiler warning for func2(p).”

#include <iostream>

using namespace std;


void func1(int x)
{
    cout << x << endl;
}


void func2(bool x)
{
    cout << x << endl;
}


int main()
{
    int x = 99;

    int *x_ptr = &x;

    //func1(x_ptr);

    func2(x_ptr);

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_10$ ./pointer_to_bool 
1

Uncommenting the call to func1(int) results in the following compiler error, but still no warning for the call to func2(bool) where a conversion is permitted.

user@ubuntu:~/cpp/part_2/chapter_10$ g++ -std=c++11 -o pointer_to_bool pointer_to_bool.cpp 
pointer_to_bool.cpp: In function ‘int main()’:
pointer_to_bool.cpp:24:16: error: invalid conversion from ‘int*’ to ‘int’ [-fpermissive]
     func1(x_ptr);
                ^
pointer_to_bool.cpp:6:6: note:   initializing argument 1 of ‘void func1(int)’
 void func1(int x)

When a floating point value is converted to int, the fractional part is discarded. Some closing remarks from Bjarne: avoid complicated expressions; if in doubt about operator precedence, use parenthesis; avoid narrowing conversions; define symbolic constants to avoid magic constants. By the last point, he means use something like const int ARRAY_SIZE = 1024 instead of using the number 1024 during array allocation. This development practice can and will make the code more maintainable and less prone to memory corruption.

Chapter 11 – Select Operations

Conditional expressions can be a concise replacement for simple if statements. For example:

#include <iostream>

using namespace std;

int main()
{   
    int a = 1;
    int b = 2;
    int max = 0;

    if (a <= b)
        max = b;
    else
        max = a;

    /* The equivalent conditional-expression */

    max = (a <= b) ? b : a;

    return 0;
}

A named object has its lifetime determined by its scope. If that is not enough and you need the object to exist independently of its scope, use new to allocate space on the free store aka the heap aka dynamic memory. A C++ implementation does not guarantee the existence of a garbage collector, so you must clean up the heap yourself with delete. A garbage collector would look for objects that are no longer needed and free the memory. If the deleted object is of a class with a destructor, that destructor is called by delete before the object’s memory is released for reuse. There are 3 core issues with the heap: leaked objects, early deletion, double deletion. Leaked objects sounds scary but all it means is that you forgot to free up the memory, so the object just exists but isn’t used anymore. This can lead to OOM “out of memory” conditions and your process will be killed by the OS. Early deletion is scary. If the object is freed (via delete), the heap space is reclaimed by the OS. If the stale pointer to that object is then used, the object’s memory could be anything. It might be the old objects data, or that area of memory could have been overwritten with new data. This would lead to unexpected results at best. Lastly, double deletion aka double free results in undefined behavior that can sometimes be leveraged during exploitation. Freeing a pointer that has already been freed results in a corrupted state in the memory allocator. It may not even be obvious to the developer that a double free is going to occur until the program is compiled and stress tested. Bjarne advises the following to guard against heap issues. First, don’t use the heap if possible; prefer scoped variables on the function stack. Second, if you must use the heap (large objects, many objects, need for global scope, etc), place the pointer into a manager object aka “handle” with a destructor. All standard library containers are provide these safety mechanisms. Have the handle be a scoped variable. Smart pointers can provide solutions to these memory management problems as well, counting references to the object and freeing it when the reference count reaches zero. They are garbage collected pointersHere is a good writeup on smart pointers, and another.

#include <memory>
#include <iostream>

using namespace std;

int main()
{   
    unique_ptr<int> ptr1 {new int(99)};

    cout << "ptr1 = " << *ptr1 << endl;

    /* Transfer ownership to ptr2 */
    unique_ptr<int> ptr2 { move(ptr1) };

    /* Should be the same address as the old ptr1 */
    cout << "ptr2 = " << *ptr2 << endl;

    /* Causes segfault because ptr1 no longer exists. */
    cout << "ptr1 = " << *ptr1 << endl;

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_11$ g++ -std=c++11 -o smart_ptr_1 smart_ptr_1.cpp 
user@ubuntu:~/cpp/part_2/chapter_11$ ./smart_ptr_1 
ptr1 = 99 at 0xc44c20
ptr2 = 99 at 0xc44c20
Segmentation fault (core dumped)

Leave a Reply