Chapter 29. Ten Major Recent Additions to C++

In This Chapter

  • Using smart pointers

  • Initializing objects with a variable-length list

  • Initializing data members inline

  • Instantiating an extern template

  • Implementing thread local storage

  • Using r-value references to avoid creating copies of objects

  • Implementing concepts

  • Defining lamda expressions

  • Defining variadic templates

  • Using typeid

Note

In this chapter, I look at ten major additions to the C++ language. Most (but not all) of these additions weren't made formal until the release of the C++ 2009 standard. All of these topics are beyond the range of an introductory book; I merely touch on these topics to give you an idea of where you might want to study next.

Use Smart Pointers

Loss of memory due to forgetting to return memory to the heap is one of the biggest sources of error in C++ programs. Both Java and C#, two followers to the C++ language, instituted a process known as garbage collection that collects unreferenced memory and returns it to the heap for you. Garbage collection has been discussed for C++ as well but has been rejected for several reasons, including the overhead it levies on your program.

The problem could be "solved" if dynamically allocated memory were tied to an object rather than a pointer. The destructor of an object is invoked when it goes out of scope. This destructor could keep track of when it was appropriate to return the memory to the heap.

The standard library includes a shared_ptr<T> template class, which allocates memory off the heap and returns it to the heap when necessary. This template class also overloads the necessary operators to give it the look and feel of an actual pointer, as demonstrated in the following code snippet:

struct MyClass
{
    int value;
};
void fn()
{
    // allocate a pointer and allocate it to an object
    shared_ptr<MyClass> ptrObj(new MyClass);

    // use the ptrObj like a real pointer to a MyClass
    // object
    ptrObj->value = 5;

    // destructor invoked when ptrObj goes out of scope
    // to return the memory to the heap
}

A MyClass object is allocated off the heap and its address assigned to a ptrObj for safekeeping. This ptrObj can be used exactly like a pointer. The shared_ptr<MyClass> class keeps track of the number of objects that reference that particular piece of memory, and when that count drops to 0 the destructor returns the memory to the heap.

Initialize Variables with a Variable-Length List

You have seen one case in which an initializer list can have a variable number of objects:

int array[] = {1, 2, 3};

In this case, the number of elements in the array is determined by simply counting the number of objects in the list. The C++ '09 definition extends this ability to other container classes through the introduction of the initializer_list<T> template class.

C++ tries to interpret any sequence of objects of the same type as an instance of initialize_list<T>. Thus, I might declare a container class as follows:

#include <initializer_list>
#include <list>
class MyContainer
{
  public:
    MyContainer(initializer_list<int> l)
    {
        nSize = l.size();

        // add each element in the initializer list to
        // our container elems
        for(auto ptr = l.begin(); ptr != l.end; ptr++)
        {
            elems.push_back(*ptr);
        }
    }

    int size() { return nSize;}

  protected:
    int nSize;
    list<int> elems;
};

// declare a MyContainer object
MyContainer myContainer1 = {1, 2, 3, 4, 5};

Notice that the constructor accepts an initializer_list object. This very simple list class provides a size() method to return the number of elements in the list along with a begin() and end() methods that return iterators just like those demonstrated in Chapter 27.

The initializer_list class extends to flexible initialization syntax available to arrays to more complex container types.

Initialize Data Members Inline

The programmer can initialize a variable when it is declared, as in the following:

int value = 10;

However, this ability did not extend to data members of classes. These could only be initialized in the constructor prior to '09:

// Version A
class MyClass
{
  public:
    MyClass() : value(10) {}

    int value;
};

The '09 definition extends initialization to data members as follows:

// Version B
class MyClass
{
  public:
    MyClass(){}

    int value = 10;
};

The compiler treats Version A and Version B exactly alike.

Instantiate an Extern Template

A template class does not generate code until it has been instantiated with a class. Once instantiated, however, it is difficult for a compiler to not repeat the process separately for every module that uses the template. Consider the following definition contained within an include file:

template <typename T>
  T max(T t1, T t2)
  {
      if (t1 > t2)
          return t1;
      return t2;
  }

Suppose that two source files, ModuleA and ModuleB, both invoke max(int, int). It should be possible for C++ to instantiate the template once and reference both modules to the same definition. This turns out to be very difficult to do.

It has always been possible to instantiate a template explicitly, for example including the following within ModuleA:

template int max<int>;

From within ModuleB (and every other module that uses max<int>) I can say:

extern template int max<int>;

This is equivalent to a prototype declaration. It tells the compiler that the max() function has been defined, but it also says that max<T> has been expanded somewhere else, in another module.

This use of extern may avoid the generation of a lot of duplicate source code caused by instantiating the same templates numerous times.

Implement Thread Local Storage

More and more computers are being offered with multicore options in which more than one CPU is included on the same piece of silicon. To get maximum advantage from a multicore processor, however, the programmer needs to divide her program to be able to execute in separate threads. Each thread can be assigned to its own processor and run independent of the other threads that make up the program.

Multithreading libraries have been available for C++ for years, but C++ '09 has taken the initial steps towards implementing multithreading within the language itself through the introduction of thread_local storage.

Variables that have global scope or static scope (see Chapter 6) are shared between all threads. Each thread gets its own copy of variables declared thread_local storage. A thread_local variable is constructed when the thread starts and its destructor is invoked when the thread exits.

The thread support library accessible through the include file thread contains functions for starting, stopping, and communicating with separate threads.

Use Rvalue References

Expressions can be divided into two types: lvalues and rvalues. An lvalue is a value that can appear on the left side of an assignment operator; an rvalue can appear only on the right. Some operators generate lvalues (the assignment operator itself, for example, returns an lvalue). Other operators, such as addition, return only rvalues. Thus, the following is not legal:

int a, b;
(a + b) = 3;  // error: a + b is an rvalue

Generally the values returned by a function are rvalues. The one big exception is that functions that return a reference to an object can appear on the left side of an assignment:

MyObject& fn1();
fn1() = MyObject();

Such assignments are performed by first creating the MyObject() and then copying it into the object referenced by fn1(). Copying the MyObject means reallocating and duplicating any resources created by the class. However, this is fairly ridiculous because we know that the MyObject created here is just a temporary anyway and will get destructed as soon as the assignment has been completed.

Rather than copy the temporary MyObject(), why not use move semantics and just "steal" any resources created by the constructor and hand them over to the object referenced by fn1()? The programmer can write much more efficient classes when she can differentiate between references to rvalues and lvalues being passed to a function.

The 2009 standard allows the programmer to express this difference as follows:

void fn(MyObject&  lhr); // argument is an lvalue
void fn(MyObject&& rhr); // argument is an rvalue

The details are beyond the scope of this book, but with judicious use of rvalue declarations, the programmer can greatly reduce the number of temporary objects created and destructed.

Implement Concepts

Multiple inheritance is often used by the programmer to tell the C++ compiler that a class has some capability, as shown in the following snippet:

class Assignable
{
  public:
    virtual Assignable& operator=(Assignable const&) = 0;
};
class MyClass : public BaseClass, public Assignable {...};

Any class that inherits from the abstract class Assignable must implement the assignment operator before it can be used to create an object. This is particularly useful in the implementation of template classes.

C++ '09 formalizes this promissory class in the form of a concept, defined as follows:

auto concept LessThanComparable<typename T>
{
    bool operator<(T, T);
}

A subsequent template definition can then rely upon this promise to implement the comparison operator:

template<typename T> requires LessThanComparable<T>
const T& min(const T& t1, const T& t2)
{
    if (t1 < t2)
        return t1;
    else
        return t2;
}

A sometimes more convenient construct allows the concept name to be used instead of the typename in the template definition as follows:

template <LessThanComparable T> class MyContainer
{
    // the following sort method uses the operator<()
    // comparison operator from class T to perform the
    // sort
    void sort();
    // ...class definition continues...
};

The concept approach generates considerably less code than using multiple inheritance for the same purpose. If you are familiar with Java or C#, you will recognize a lot of similarities between the role played by concepts in C++ and interfaces in those languages.

Define Lamda Expressions

Lambda expressions are a way to define function objects that are intended to be used locally in a concise way that does not interfere with the overall logic flow. Consider the following code snippet taken from the C++ '09 proposal.

double dUpperLimit = 1.1 * dMinSalary;
std::find_if(employees.begin(),employees.end(),
    [&](const employee& e)
              { return e.salary() >= dMinSalary && e.salary() < dUpperLimit; });

Here employees is a collection of Employee objects. The find_if() function takes as its arguments an iterator that points to where in the list to start (in this case, the beginning of the list), an iterator that points to where in the list to stop (in this case, the end of the list) and an object that implements some selection criteria.

Define Variadic Templates

Variadic templates allow the programmer to define a template with a variable number of classes:

template<typename... Attributes> class TableRow;

Now the class TableRow can be instantiated with any number of attributes.

class TableRow<int, string, std:list<Grades>> Student;

Note: C and C++ have always allowed variadic functions, functions with an unspecified number of arguments (printf() is an example of such a function). This extends the concept to class definitions.

Use typeid()

Normally the programmer should differentiate the difference between the declared type and the runtime type through virtual member functions:

class Base
{
  public:
    virtual aMethod();
};
class Derived : public Base
{
  public:
    virtual aMethod();
};

void fn(Base* pB)
{
    // which of the above functions is invoked is
// determined at runtime based upon the runtime
    // type
    pB->aMethod();
}

However, it is possible to ask an object for its runtime type using the keyword typeid, which returns a reference to a constant of type type_info. Thus, the programmer could differentiate between subclasses of Base by comparing the value returned from typeid as follows:

void fn(Base* pB)
{
    // differentiate between type of Base object
    // using the typeid
    const type_info& ti = typeid(*pB);
    if (ti == typeid(Base))
    {
         // it's a Base object
    }
    else if (ti == typeid(Derived))
    {
        // it's actually a Derived object
    }
}

I stress could make the determination using typeid only because it's generally not a good idea; however, it is possible. This technique is especially useful when you are creating debug code and you don't want to modify either the Base or Derived classes because they are in separate modules that you don't control and can't modify.

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

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