So, what is new in modern C++ in comparison to the old one? There are so many changes in modern C++ compared to the old one, and the book pages will dramatically increase if we discuss all of them. However, we will discuss the new features in modern C++, which we should know about, to make us more productive in coding activities. We will discuss several new keywords, such as auto
, decltype
, and nullptr
. We will also discuss the enhancement of the begin()
and end()
function that has now become a non-member class function. We will also discuss the augmented support for the for-each
technique to iterate over collections using the range-based for loop
techniques.
The next few subsections in this chapter will also discuss the new features of modern C++, namely Lambda expressions, smart pointers, and tuples, which were just added in the C++11 release.
Prior to the modern C++, the C++ language has a keyword named auto
that is used to explicitly specify that the variable should have automatic duration. The automatic duration that adheres to the variable will create the variable at the point of definition (and initialized, if relevant) and destroy the variable when the block they are defined in is exited. For instance, the local variable will be created when it is defined at the beginning of the function and destroyed when the program exits the function where the local variable is there.
Since C++11, the auto
keyword is used to tell the compiler to deduce the actual type of a variable that is being declared from its initializer. And since C++14, the keyword can also be applied to a function to specify the return type of the function that is a trailing return type. Now, in modern C++, the use of the auto
keyword to specify the automatic duration is abolished since all variables are set to automatic duration by default.
The following is an auto.cpp
code demonstrating the use of the auto
keyword in the variables. We will define four variables with the auto
keyword, and then find out the data type for each variable using the typeid()
function. Let's take a look:
/* auto.cpp */ #include <iostream> #include <typeinfo> int main() { std::cout << "[auto.cpp]" << std::endl; // Creating several auto-type variables auto a = 1; auto b = 1.0; auto c = a + b; auto d = {b, c}; // Displaying the preceding variables' type std::cout << "type of a: " << typeid(a).name() << std::endl; std::cout << "type of b: " << typeid(b).name() << std::endl; std::cout << "type of c: " << typeid(c).name() << std::endl; std::cout << "type of d: " << typeid(d).name() << std::endl; return 0; }
As we can see in the preceding code, we have an a
variable that will store the integer
value and have a b
variable that will store the double
value. We calculate the addition of a
and b
and store the result in variable c
. Here, we expect that c
will store the double
object since we add the integer
and double
object. The last is the d
variable that will store the initializer_list<double>
data type. When we run the preceding code, we will see the following output on the console:
As can be seen in the preceding snapshot, we are just given the first character of the data type, such as i
for integer
, d
for double
, and St16initializer_listIdE
for initializer_list<double>
, that is the last lowercase d
character that stands for double
.
Note
We may have to enable the Run-Time Type Information (RTTI) feature in our compiler options to retrieve the data type object. However, GCC has enabled the feature by default. Also, the output of the use of the typeid()
function depends on the compiler. We may get the raw type name or just a symbol as we did in the preceding example.
Besides, for variable, as we discussed earlier, the auto
keyword can also be applied to a function to deduce a function's return type automatically. Suppose we have the following trivial function named add()
to calculate the addition of two parameters:
int add(int i, int j) { return i + j; }
We can refactor the preceding method to use the auto
keyword, as we can see in the following lines of code:
auto add(int i, int j) { return i + j; }
Similar to the auto-type variable, the compiler can decide the correct return type based on the returned value of the function. And, as shown in the preceding code, the function will indeed return the integer value since we just add two integer values.
Another feature that uses the auto
keyword in modern C++ is trailing the return type syntax. By using this feature, we can specify the return type, the rest of the function prototype, or function signature. From the preceding code, we can refactor it to use the feature as follows:
auto add(int i, int j) -> int { return i + j; }
You might ask me why we have to specify the data type again after the arrow symbol (->
), even though we have used the auto
keyword. We will find the answer when we cover the decltype
keyword in the next section. Also, by using this feature, we can now refactor the preceding auto.cpp
code a little bit by modifying the syntax of the main()
method, instead of the following syntax of main()
function signature:
int main() { // The body of the function }
We can change the signature syntax into the following line of code:
auto main -> int { // The body of the function }
Now, we will see all of our code in this book using this trailing return type feature to apply the modern C++ syntax.
We discussed in the preceding section that the auto
keyword can automatically deduce the type of the variable based on the type of values it stores. The keyword can also deduce the function's return type based on the type of its return value. Now, let's combine the auto
keyword and the decltype
keyword to gain the power of modern C++.
Before we combine the two keywords, we will find out what the decltype
keyword is used for--it is used for asking the type of an object or an expression. Let's take a look at the following several lines of trivial variable declaration:
const int func1(); const int& func2(); int i; struct X { double d; }; const X* x = new X();
Now, based on the preceding code, we can declare other variables using the decltype
keyword as follows:
// Declaring const int variable // using func1() type decltype(func1()) f1; // Declaring const int& variable // using func2() type decltype(func2()) f2; // Declaring int variable // using i type decltype(i) i1; // Declaring double variable // using struct X type decltype(x->d) d1; // type is double decltype((x->d)) d2; // type is const double&
As we can see in the preceding code, we can specify the type of an object based on another object's type. Now, let's suppose we need to refactor the preceding add()
method to become a template. Without the auto
and decltype
keyword, we will have the following template implementation:
template<typename I, typename J, typename K> K add(I i, J j) { return i + j; }
Fortunately, since the auto
keyword can specify the return type of the function, which is a trailing return type, and the decltype
keyword can deduce the type based on the expression, we can refactor the preceding template as follows:
template<typename I, typename J> auto add(I i, J j) -> decltype(i + j) { return i + j; }
To prove, let's compile and run the following decltype.cpp
code. We will use the following template to calculate the addition of two different value types--integer
and double
:
/* decltype.cpp */ #include <iostream> // Creating template template<typename I, typename J> auto add(I i, J j) -> decltype(i + j) { return i + j; } auto main() -> int { std::cout << "[decltype.cpp]" << std::endl; // Consuming the template auto d = add<int, double>(2, 2.5); // Displaying the preceding variables' type std::cout << "result of 2 + 2.5: " << d << std::endl; return 0; }
The compilation process should run smoothly without error. We will see the following output on the screen if we run the preceding code:
As we can see, we have successfully combined the auto
and decltype
keyword to create a template simpler than we usually do before the modern C++ is announced.
Another new feature in modern C++ is a keyword named nullptr
that replaces the NULL
macro to represent a null pointer. Now, there's no ambiguity in the use of the NULL
macro for zero numeric or a null pointer. Let's suppose we have the following two method's signature in our declaration:
void funct(const char *); void funct(int)
The former function will pass a pointer as the argument and the latter will pass the integer number as its argument. Then, we invoke the funct()
method and pass the NULL
macro as the parameter, as shown here:
funct(NULL);
What we intend to call is the former function. However, since we pass the NULL
parameters, which is basically defined as 0
, the latter function will be invoked. In modern C++, we can use the nullptr
keyword to ensure that we will pass a null pointer to the argument. The invocation of the funct()
method should be as follows:
funct(nullptr);
Now the compiler will invoke the former function since it passes a null pointer to the argument, and this is what we expect. There will be no ambiguity anymore, and it will avoid unnecessary future problems.
Prior to modern C++, to iterate a sequence, we call the begin()
and end()
member method of each container. For array, we can iterate its element by iterating the index. Since C++11, the language has a non-member function--begin()
and end()
--to retrieve the iterator of the sequence. Let's suppose we have an array of the following elements:
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
When the language doesn't have the begin()
and end()
function, we need to iterate the elements of the array using the index we can see in the following lines of code:
for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) // Do something to the array
Fortunately, using the begin()
and end()
function, we can refactor the preceding for
loop to become as follows:
for (auto i = std::begin(arr); i != std::end(arr); ++i) // Do something to the array
As we can see, the use of the begin()
and end()
function creates a compact code since we don't need to worry about the length of the array because the iterator pointer of begin()
and end()
will do it for us. For comparison, let's take a look at the following begin_end.cpp
code:
/* begin_end.cpp */ #include <iostream> auto main() -> int { std::cout << "[begin_end.cpp]" << std::endl; // Declaring an array int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Displaying the array elements // using conventional for-loop std::cout << "Displaying array element using conventional for- loop"; std::cout << std::endl; for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) std::cout << arr[i] << " "; std::cout << std::endl; // Displaying the array elements // using non-member begin() and end() std::cout << "Displaying array element using non-member begin() and end()"; std::cout << std::endl; for (auto i = std::begin(arr); i != std::end(arr); ++i) std::cout << *i << " "; std::cout << std::endl; return 0; }
To prove the preceding code, we can compile the code, and, when we run it, the following output should be displayed on the console screen:
As we can see in the screenshot, we've got the exact same output when we use the conventional for-loop
or begin()
and end()
functions.
In the modern C++, there is a new feature that is augmented to support the for-each
technique to iterate over collections. This feature is useful if you want to do something to the elements of a collection or array without caring about the number of elements or the indexes. The syntax of the feature is also simple. Suppose we have an array named arr
and we want to iterate each element using the range-based for loop
technique; we can use the following syntax:
for (auto a : arr) // Do something with a
So, we can refactor our preceding begin_end.cpp
code to use range-based for loop
as we can see in the following code:
/* range_based_for_loop.cpp */ #include <iostream> auto main() -> int { std::cout << "[range_based_for_loop.cpp]" << std::endl; // Declaring an array int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // Displaying the array elements // using non-member begin() and end() std::cout << "Displaying array element using range-based for loop"; std::cout << std::endl; for (auto a : arr) std::cout << a << " "; std::cout << std::endl; return 0; }
The syntax we see in the preceding code is simpler now. If we compile the preceding code, we should find no error and, if we run the code, we should see the following output on the console screen:
We now have a new technique to iterate over the collection without caring about the indexes of the collection. We will keep using it in this book.