C++ 11

C++11, officially known as ISO/IEC 14882:2011, was released in 2011 and was a significant milestone in the evolution of the C++ programming language.

It introduced a plethora of new features and enhancements that made C++ more powerful.


Features Introduced in C++ 11

Here are the most important features introduced in C++ 11:

  • The auto keyword
  • Range based for loop
  • Lambda Expressions
  • Smart Pointers
  • The constexpr keyword
  • The nullptr keyword
  • Type traits
  • Thread support
  • Delegating constructor
  • Deleted and defaulted functions

Let's start by exploring the auto keyword.


C++ auto Keyword

The auto keyword allows C++11 to automatically deduce the type of the variable from its initializer.

#include <iostream>
using namespace std;

int main() {

// x is deduced as int auto x = 42; // pi is deduced as double auto pi = 3.1415926535;
cout << "x: " << x << endl; cout << "pi: " << pi << endl; return 0; }

Output

x: 42
pi: 3.14159

Here, the compiler is capable of deducing the type of both variable x and pi from their initializer, so we don't need to explicitly mention their type. However, when using auto, the variable must be always initialized.

auto x; // error: must be always initialized

Note: auto is a type placeholder in C++, not a type itself. So, it cannot be used in casts or operators like sizeof or typeid.


Range Based for Loops

Range based for loops in C++ executes a loop for a range.

Syntax

for (range_initialization : range_container) { // loop statements }

Here,

  • range_initialization - creates an iterating variable.
  • range_container - container to iterate over.

For example,

vector<int> numbers = {1, 2, 3};

// loop through the numbers vector
for (int num : numbers) {

    // print vector element
    cout << num << endl;
}

Note: range-based-loops should be preferred over normal for loops whenever possible to avoid out of bound errors.


Example 1: C++ Range Based for Loop

#include <iostream>
#include <vector>
using namespace std;

int main() {

    // create a vector of integers
    vector<int> numbers = {1, 2, 3, 4, 5};

// use a range-based for loop // to iterate through the vector for (const auto& num : numbers) { cout << num << " "; }
return 0; }

Output

1 2 3 4 5 

Here, we have declared the iterating variable num and iterated over the container numbers using a range based for loop.


Lambda Expressions

Lambda expressions allow you to create anonymous functions in a concise manner. They are especially useful when you need to pass small functions as arguments to other functions.

Syntax

[capture_clause] (parameter_list) -> return_type {
    // lambda body: Code to be executed
}

Here,

  • capture_clause - specifies which variables from the surrounding scope are accessible within the lambda function.
  • parameter_list - defines the parameters the function accepts.
  • -> return_type - declares the return type of the function.

Note: We can omit return_type for auto deduction of types unless there are multiple possible return values of different types.

To learn more, visit our C++ Lambda tutorial.


Example 2: C++ Lambda Expression

#include <iostream>
using namespace std;

int main() {

// define a lambda function named 'add' // that takes two integers and returns their sum auto add = [] (int a, int b) { return a + b; }; // call the lambda with arguments 3 and 4 int result = add(3, 4);
// print the result. cout << "Result: " << result << endl; return 0; }

Output

Result: 7

Here, we created a lambda function add() that takes in two arguments a and b and returns their sum.


Smart Pointers

C++11 introduced smart pointers that automatically manage memory and help prevent memory leaks. Basically, a smart pointer automatically releases the memory it manages when it goes out of scope.

Smart pointers are of two types:

  • Unique Pointers have exclusive ownership of the objects they point to.
  • Shared Pointers allow multiple shared pointers to own a single object.

Syntax

// unique pointer
std::unique_ptr<data_type> unique_pointer = std::make_unique<data_type>(args...);

// shared pointer
std::shared_ptr<Type> shared_pointer = std::make_shared<data_type>(args...);

Note: We need to import the <memory> header file to use smart pointers.


Example 3: C++ Smart Pointers

#include <iostream>
#include <memory>
using namespace std; int main() {
// create a shared pointer to an integer with value 42 shared_ptr<int> shared_pointer = make_shared<int>(42); // create a unique pointer to a double with value 3.14 unique_ptr<double> unique_pointer = make_unique<double>(3.14);
// print the value pointed to by shared_ptr cout << "shared_pointer: " << *shared_pointer << endl; // print the value pointed to by unique_ptr cout << "unique_pointer: " << *unique_pointer << endl; return 0; }

Output

shared_pointer: 42
unique_pointer: 3.14

Move Semantics

Move semantics allows the resources owned by an object to be moved into another object instead of copying them. This optimizes performance by avoiding deep copies.

We use the move() function to implement move semantics. For example,

#include <iostream>
#include <vector>
using namespace std;

int main() {

    // create an integer vector
    vector<int> source = {1, 2, 3};

// move the contents of source to another vector vector<int> destination = move(source);
// print the destination vector cout << "Destination Vector Contents: "; for (const int num : destination) { cout << num << " "; } // print the size of the destination vector cout << "\nDestination Vector Size: " << destination.size(); return 0; }

Output

Destination Vector Contents: 1  2  3  
Destination Vector Size: 3

Here, we have used the move() function to move the contents from source to destination.

vector<int> destination = move(source);

C++ constexpr Keyword

The constexpr keyword allows you to specify that a variable or function can be evaluated at compile-time. For example,

constexpr int square(int x) {
    return x * x;
}

// computed at compile-time
int result = square(5);

Here, we have used the constexpr keyword with the square() function. As a result, square(5) can be evaluated at compile time rather than run time.

This can boost the performance of the code and ensures that the expressions are initialized with a value that is known at compile time.


Null Pointer

The introduction of nullptr provides a safer alternative to using NULL for null pointers.

int* ptr = nullptr;

Note: Always prefer nullptr over NULL because of the safety nullptr provides over NULL.


Type Traits

In C++, type traits are a group of templates that are used to gather information about types at compile time, and are a powerful tool for template metaprogramming.

This means that before your program runs, the compiler can understand things about the types you're using—like whether a type is an integer, if it can be copied, or if it's a class with a certain function.

They are part of the Standard Template Library (STL) and included within the <type_traits> header file.

Type traits are implemented using class templates or function templates. Here's a sample code of a function template.

template <typename T>
void process(T value) {

    if (std::is_pointer<T>::value) {
        // handle pointers
    }
    else if (std::is_integral<T>::value) {
        // handle integers
    }
}

Thread Support

C++11 added a standardized threading library that allows you to create and manage threads.

#include <iostream>
#include <thread>

using namespace std;

void sayHello(){
  cout << "Hello, from the spawned thread\n";
}

int main(){
    // create a std::thread object
    std::thread t(sayHello);

    std::cout << "Hello, from the main thread\n";

    // wait for the thread to complete its job
    t.join();

}

Output:

Hello, from the main thread
Hello, from the spawned thread

Note: The output is execution dependent, so you may not get the same output every time you run it.

Every thread has to have an initial function, from where the execution of the new thread begins.

Thus we created a std::thread object t by passing in a function sayHello, which we want the thread to execute.

The program now has 2 threads - one executing the main function and the other executing the sayHello function.


Delegating Constructors

In C++11, a constructor may call another constructor of the same class. For example,

#include <iostream>
using namespace std;

class Complex {
    int img;
    int real;

public:

    // constructor #1 (target constructor)
    Complex(int i, int r) : img(i), real(r) {}

// constructor #2 (delegating constructor) // pass two zeroes as arguments to target constructor Complex() : Complex(0, 0) { cout << "Delegating constructor" << endl; cout << "img = " << img << endl; cout << "real = " << real << endl; }
}; int main() { // create an instance of class Complex // using the delegating constructor Complex obj; return 0; }

Output

Delegating constructor
img = 0
real = 0

Here, the delegating constructor invokes the target constructor.


Deleted and Defaulted Functions

1. Deleted Function

They are member functions that have been intentionally marked as deleted. This means that the functions cannot be used, and any attempt to call them will result in a compilation error.

Deleting functions is useful when you want to prevent certain operations on your class.

class My_Class {
public:

    // delete the default constructor
    My_Class() = delete;

    // delete a member function
    void do_something() = delete; 
};

2. Defaulted Function

They are used to explicitly request the compiler to generate default implementations for certain member functions.

This is particularly useful when you want to take advantage of the compiler-generated versions while still providing custom behavior for other functions.

class Another_Class {
public:

    // use the compiler-generated default constructor
    Another_Class() = default;

    // define a custom constructor
    Another_Class(int x) : value(x) {} 

private:
    int value;
};

Example 4: C++ Deleted and Defaulted Functions

class Example {
public:

    // defaulted default constructor
    Example() = default;
    
    // deleted copy constructor
    Example(const Example&) = delete; 
};

int main() {

    // allowed: default constructor is used
    Example obj1; 
    
    // error: copy constructor is deleted
    // Example obj2 = obj1;

    return 0;
}

Here,

  • We can create an object of the Example class using the default constructor.
  • We cannot create a copy of an object of Example because the copy constructor is deleted.
Did you find this article helpful?