Lesson 29. Going Forward

You have learned the basics of C++ programming. In fact, you have gone beyond theoretical boundaries in understanding how using the Standard Template Library (STL), templates, and the Standard Library can help you write efficient and compact code. It is time to give performance a look and gain a perspective on programming best practices.

In this lesson, you learn

Image How your C++ application can best utilize the processor’s capabilities

Image Threads and multithreading

Image Best practices in programming in C++

Image New Features expected in C++17

Image Improving your C++ skills beyond this book

What’s Different in Today’s Processors?

Until recently, computers got faster by using processors that featured faster processing speeds, measured in hertz (Hz), megahertz (Mhz), or gigahertz (GHz). For instance, Intel 8086 (see Figure 29.1) was a 16-bit microprocessor launched in 1978 with a clock speed of about 10MHz.

Image

FIGURE 29.1 The Intel 8086 microprocessor.

Those were the days when processors got significantly faster at regular intervals and so did your C++ application. It was easy to rely on a waiting game to make use of improved hardware performance and improving your software’s responsiveness through it. Although today’s processors are getting faster, the true innovation is in the number of cores they deploy. At the time of writing this book, even popular smartphones feature 64-bit processors with four cores and more processing capacity than a desktop computer from a decade ago.

You can think of a multicore processor as a single chip with multiple processors running in parallel within it. Each processor has its own L1 cache and can work independently of the other.

A faster processor increasing the speed of your application is logical. How do multiple cores in a processor help? Each core is evidently capable of running an application in parallel, but this doesn’t necessarily make your application run any faster unless you have programmed it to consume this new capability. Single-threaded C++ applications of the types you have seen this far are possibly missing the bus as far as using multicore processing capabilities go. The applications run in one thread, and hence on only one core, as shown in Figure 29.2.

Image

FIGURE 29.2 A single-threaded application in a multiple-core processor.

If your application executes all use cases in a serial order, the operating system (OS) will possibly give it only as much time as other applications in the queue and it will occupy only one core on the processor. In other words, your application is running on a multicore processor in the same way as it would do in those years gone by.

How to Better Use Multiple Cores

The key is in creating applications that are multithreaded. Each thread runs in parallel, allowing the OS to let the threads run on multiple cores. Although it is beyond the scope of this book to discuss threads and multithreading in great detail, I can just touch this topic and give you a head start toward high-performance computing.

What Is a Thread?

Your application code always runs in a thread. A thread is a synchronous execution entity where statements in a thread run one after another. The code inside main() is considered to execute the main thread of the application. In this main thread, you can create new threads that can run in parallel. Such applications that are comprised of one or more threads running in parallel in addition to the main thread are called multithreaded applications.

The OS dictates how threads are to be created, and you can create threads directly by calling those APIs supplied by the OS.


Tip

C++ since C++11 specifies thread functions that take care of calling the OS APIs for you, making your multithreaded application a little more portable.

If you plan to be writing your application for only one OS, check your OS’s APIs on creating multithreaded applications.



Note

The actual act of creating a thread is an OS-specific functionality. C++ tries to supply you with a platform-independent abstraction in the form of std::thread in header <thread>.

If you are writing for one platform, you are better off just using the OS-specific thread functions.

Should you need portable threads in your C++ application, do look up Boost Thread Libraries at www.boost.org.


Why Program Multithreaded Applications?

Multithreading is used in applications that need to do multiple sessions of a certain activity in parallel. Imagine that you are one of 10,000 other users making a purchase on Amazon’s web portal at a particular moment. Amazon’s web server can of course not keep 9,999 users waiting at a time. What the web server does is create multiple threads, servicing multiple users at the same time. If the web server is running on a multiple-core processor or a multiple processor cloud, the threads can extract the best out of the available infrastructure and provide optimal performance to the user.

Another common example of multithreading is an application that does some work in addition to interacting with the user, for instance via a progress bar. Such applications are often divided into a User Interface Thread that displays and updates the user interface and accepts user input, and the Worker Thread that does the work in the background. A tool that defragments your disk is one such application. After you press the start button, a Worker Thread is created that starts with the scan and defragmenting activity. At the same time, the User Interface Thread displays progress and also gives you the option to cancel the defragmentation. Note that for the User Interface Thread to show progress, the Worker Thread that does the defragmentation needs to regularly communicate the same. Similarly, for the Worker Thread to stop working when you cancel, the User Interface Thread needs to communicate the same.


Note

Multithreaded applications often need threads to “talk” to each other so that the application can function as a unit (and not a collection of runaway threads that do their stuff irrespective of the other).

Sequence is important, too. You don’t want the User Interface Thread to end before the defragmenting Worker Thread has ended. There are situations where one thread needs to wait on another. For instance, a thread that reads from a database should wait until the thread that writes is done.

The act of making threads wait on another is called thread synchronization.


How Can Threads Transact Data?

Threads can share variables. Threads have access to globally placed data. Threads can be created with a pointer to a shared object (struct or class) with data in it, shown in Figure 29.3.

Image

FIGURE 29.3 Worker and user interface threads sharing data.

Different threads can communicate by accessing or writing data that is stored in a location in memory that can be accessed by them all and is hence shared. In the example of the defragmenter where the Worker Thread knows the progress and the User Interface Thread needs to be informed of it, the Worker Thread can constantly store the progress in percentage at an integer that the User Interface Thread uses to display the progress.

This is a simple case, though—one thread creates information and the other consumes it. What would happen if multiple threads wrote and read from the same location? Some threads might start reading data when some other threads have not finished writing them. The integrity of the data in question would be compromised.

This is why threads need to be synchronized.

Using Mutexes and Semaphores to Synchronize Threads

Threads are OS-level entities, and the objects that you use to synchronize them are supplied by the OS, too. Most operating systems provide you with semaphores and mutexes for performing thread synchronization activity.

You use a mutex, a mutual exclusion synchronization object, to ensure that one thread has access to a piece of code at a time. In other words, a mutex is used to bracket a section of code where a thread has to wait until another thread that is currently executing it is done and releases the mutex. The next thread acquires the mutex, does its job, and releases the same. C++ starting with C++11 supplies you with an implementation of a mutex in class std::mutex available via header <mutex>.

Using semaphores, you can control the number of threads that execute a section of code. A semaphore that allows access to only one thread at a time is also called a binary semaphore.

Problems Caused by Multithreading

Multithreading with its need for good synchronization across threads can also cause a good number of sleepless nights when this synchronization is not effective (read: buggy). Two of the most frequent issues that multithreaded applications face are the following:

Image Race conditions—Two or more threads trying to write to the same data. Who wins? What is the state of that object?

Image Deadlock—Two threads waiting on each other to finish resulting in both being in a “wait” state. Your application is hung.

You can avoid race conditions with good synchronization. In general, when threads are allowed to write to a shared object, you must take extra care to ensure that

Image Only one thread writes at a time.

Image No thread is allowed to read that object until the writing thread is done.

You can avoid deadlocks by ensuring that in no situation do two threads wait on each other. You can either have a master thread that synchronizes worker threads or program in a way such that tasks are distributed between threads and result in clear workload distribution. A thread A can wait on B, but B should never need to wait on A.

Programming multithreaded applications is a specialization in itself. Hence, it is beyond the scope of this book to explain this interesting and exciting topic to you in detail. You should either refer to the plenty of online documentation available on the topic or learn multithreading by hands-on programming. Once you master it, you will automatically position your C++ applications optimally as far as using multicore processors being released in the future goes.

Writing Great C++ Code

C++ has not only evolved significantly since the days it was first conceived, but standardization efforts made by major compiler manufacturers and the availability of utilities and functions help you write compact and clean C++ code. It is indeed easy to program readable and reliable C++ applications.

Here is a short list of best practices that help you create good C++ applications:

Image Give your variables names that make sense (to others as well as to you). It is worth spending a second more to give variables better names.

Image Always initialize variables such as int, float, and the like.

Image Always initialize pointer values to either NULL or a valid address—for instance, that returned by operator new.

Image When using arrays, never cross the bounds of the array buffer. This is called a buffer overflow and can be exploited as a security vulnerability.

Image Don’t use char* string buffers or functions such as strlen() and strcpy(). std::string is safer and provides many useful utility methods including ones that help you find the length, copy, and append.

Image Use a static array only when you are certain of the number of elements it will contain.

If you are not certain of it, choose a dynamic array such as std::vector.

Image When declaring and defining functions that take non-POD (plain old data) types as input, consider declaring parameters as reference parameters to avoid the unnecessary copy step when the function is called.

Image If your class contains a raw pointer member (or members), give thought to how memory resource ownership needs to be managed in the event of a copy or assignment. That is, consider programming copy constructor and copy assignment operator.

Image When writing a utility class that manages a dynamic array or the like, remember to program the move constructor and the move assignment operator for better performance.

Image Remember to make your code const-correct. A get() function should ideally not be able to modify the class’s members and hence should be a const. Similarly, function parameters should be const-references, unless you want to change the values they contain.

Image Avoid using raw pointers. Choose the appropriate smart pointers where possible.

Image When programming a utility class, take effort in supporting all those operators that will make consuming and using the class easy.

Image Given an option, choose a template version over a macro. Templates are typesafe and generic.

Image When programming a class that will be collected in a container, such as a vector or a list, or used as a key element in a map, remember to support operator< that will help define the default sort criteria.

Image If your lambda function gets too large, you should possibly consider making a function object of it—that is, a class with operator() as the functor is reusable and a single point of maintenance.

Image Never take the success of operator new for granted. Code that performs resource allocation should always be made exception safe—bracketed within try with corresponding catch() blocks.

Image Never throw from the destructor of a class.

This is not an exhaustive list, but it covers some of the most important points that will help you in writing good and maintainable C++ code.

C++17: Expected Features

One of the great things about C++ is that the Standards Committee is active and constantly improving the language. Just like its predecessor, C++11, C++17 is expected to usher in the next wave of major new features to the language. Let us study some features that are most likely to make it to C++ when the new standard is officially ratified in 2017.


Note

The features discussed in the following pages are likely to make it to the standard but aren’t currently part of it—it is likely that your favorite compiler partially supports some features and doesn’t support all.

Additionally, it is unlikely but not impossible that the final version of C++17 will not support all the features introduced here, even though at the time of writing this book it is expected to.


if and switch Support Initializers

This is a small but significant extension to the if and switch statement syntax, and can be expressed as

if (initializer; condition)
{
   // statements to execute if condition evaluates true
}

Or

switch(initializer; condition)
{
   // cases here
}

The variable declared in the initializer statement is destroyed at the end of the if statement. When used on the following code taken from Listing 20.3:

auto pairFound = mapIntToStr.find(key);
if (pairFound != mapIntToStr.end())
{
   cout << "Key " << pairFound->first << " points to Value: ";
   cout << pairFound->second << endl;
}

This feature improves it to

if (auto pairFound = mapIntToStr.find(key); pairFound != mapIntToStr.end())
{
   cout << "Key " << pairFound->first << " points to Value: ";
   cout << pairFound->second << endl;
}

This is more than a reduction of a line of code. It ensures that the variable pairFound that is needed only in the if block isn’t available outside it, restricting scope to the minimum required. Additionally, if you were to copy and paste this improved if block, you would have taken the required logic in full.

Copy Elision Guarantee

When you initialize a variable to the return value of a function, it is possible that your compiler will create a temporary copy of the integer returned by the function ReturnInt() before initializing variable num to it:

int num = ReturnInt();

C++17 requires the compiler to elide this temporary copy; that is, to avoid one.

std::string_view Avoids Allocation Overheads

Consider a function that accepts a std::string as a parameter:

void DisplayString (const std::string& strIn)
{
   cout << strIn << endl;
}

When invoked using a string literal, the string literal “Hello World!” is first converted into a temporary std::string that is consumed by the function DisplayString():

DisplayString(“Hello World!”);

This temporary conversion is a performance overhead that can be avoided by using std::string_view instead:

void DisplayString (std::string_view& strIn)
{
   cout << strIn << endl;
}

A string literal will not incur allocation overhead when being passed to a function that accepts a std::string_view as argument.

std::variant As a Typesafe Alternative to a union

Unions are explained in Lesson 9, “Classes and Objects.” One of the problems with the union is that it enables its content to be interpreted as any other data type supported by the union; for example:

union SimpleUnion
{
   int num;
   double preciseNum;
};

You may instantiate this union for a double, yet use it as an integer:

SimpleUnion u1;
u1.preciseNum = 3.14; // union stores a double
int num2 = u1.num; // works, but u1 contained a double!

C++17 provides the programmer with the std::variant, a typesafe alternative to the union:

variant<int, double> varSafe;
varSafe = 3.14; // variant stores double
double pi = get<double>(varSafe); // 3.14
double pi2 = get<1>(varsafe); // 3.14
get<char>(varSafe); // compile fails: no char in variant
get<2>(varSafe); // compile fails: variant with two types, not three
try
{
   get<int>(varSafe); // throws exception as variant stores double
}
catch (bad_variant_access&) { // exception handler code }

Conditional Code Compilation Using if constexpr

This feature is similar to an if-else construct with the exception that the if condition is evaluated at compile time and the code in the if block (or the accompanying else) is compiled only if the condition is satisfied at compile time.

#include <type_traits>
#include <iostream>
#include <iomanip>

using namespace std;

template <typename T>
void DisplayData(const T& data)
{
   if constexpr (is_integral<T>::value)
      cout << "Integral data: " << data << endl;
   else if constexpr (is_floating_point<T>::value)
      cout << setprecision(15) << "Floating point data: " << data << endl;
   else
      cout << "Unidentified data: " << data << endl;
}

Given DisplayData(15), the C++17-compliant compiler would compile only the following line:

cout << "Integral data: " << data << endl;

Given DisplayData(“Hello World!”), the compiler would compile only the following, as the function has been invoked with a type that triggers the else block:

cout << "Unidentified data: " << data << endl;

Combined with automatic return type deduction, introduced in Lesson 7, “Organizing Code with Functions,” this is a powerful feature that can potentially allow a function to return values of different types depending on the path the compiler executes.

Improved Lambda Expressions

Lambda functions are expected to see the following improvements:

Image They will be supported inside constexpr functions.

Image They will be allowed to capture a copy of *this using the syntax [*this].

Automatic Type Deduction for Constructors

As of C++14, you would declare a pair combining an integer and a floating point type, like this:

std::pair<int, double> pairIntToDb (3, 3.14159265359);

C++17 will allow a simplification of the line to

std::pair pairIntToDb (3, 3.14159265359);

The type deduction of the template arguments for constructors will be automatic.

template<auto>

This extends a less-used feature that a template argument may contain a value that is used at compile time. For example, the std::array is a container that models fixed-size arrays available starting with C++11. It would be used to model an array of 10 integers, like this:

std::array<int, 10> myTenNums;

The template declaration of class std::array is similar to the following:

template <class T, std::size_t N> struct array;

C++17 will allow a simplification of the template parameter type that accepts array size to auto, such that the following would be a perfectly valid and usable array:

template <class T, auto N> struct array;

Learning C++ Doesn’t Stop Here!

Congratulations, you have made great progress in learning C++. The best way to continue is to code and code more! C++ is a sophisticated language. The more you program, the higher will be your level of understanding of how it all works behind the scenes.

Online Documentation

You are encouraged to learn more about the signatures of STL containers, their methods, their algorithms, and their functional details using online resources and documents. One popular site with structured resources is www.cppreference.com/.

Communities for Guidance and Help

C++ has rich and vibrant online communities. Enroll yourself at sites such as StackOverflow (www.StackOverflow.com), CodeGuru (www.CodeGuru.com), or CodeProject (www.CodeProject.com) to have your technical queries inspected and answered by the community.

When you feel confident, feel free to contribute to these communities. You will find yourself answering challenging questions and learning a lot in the process.

Summary

This concluding lesson is actually an opening page in your quest to learn C++! Having come this far, you have learned the basics and the advanced concepts of the language.

In this lesson, you learned the theoretical basics of multithreaded programming. You learned that the only way you can extract the best from multicore processors is to organize your logic in threads and allow parallel processing. You know that there are pitfalls in multithreaded applications and ways to avoid them. Last but not the least, you learned some basic C++ programming best practices. You know that writing good C++ code is not only about using advanced concepts, but also about giving variable names that others understand, handling exceptions to take care of the unexpected, and using utility classes such as smart pointers instead of raw ones. You are now ready to take a leap into the world of professional C++ programming.

Q&A

Q I am quite happy with the performance of my application. Should I still implement multithreaded capabilities?

A Not at all. Not all applications need to be multithreaded. Rather only those that need to perform a task concurrently or that serve many users in parallel.

Q Why should I bother about C++11 and C++14, instead of simply using the old style of programming?

A C++11 and C++14 bring changes that make programming in C++ simple.

Keywords such as auto save you long and tedious iterator declarations, and lambda functions make your for_each() construct compact without the need for a function object. So, the benefits in programming C++14 are already significant, and well-written programs are shorter and easier to maintain than those compliant with older versions of the C++ standard.

Workshop

The Workshop contains quiz questions to help solidify your understanding of the material covered. Try to answer the questions before checking the answers in Appendix E.

Quiz

1. My image processing application doesn’t respond when it is correcting the contrast. What should I do?

2. My multithreaded application allows for extremely fast access to the database. Yet, sometimes I see that the data fetched is garbled. What am I doing wrong?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.207.144