Skip to content

mandliya/modern-cpp-concepts

Repository files navigation

Modern C++ Concepts

Concept 1: Variable Initialization in C++11/14 (Code)

C++ Parsing Problem

classExampleClass{
private:
intm_Member;
public:
ExampleClass() =default;
MyClass(constMyClass& rhs) =default;
};

intmain()
{
ExampleClass objectA;
ExampleClassobjectB(ExampleClass());//<----- Parasing this line is the reason for error
return0;
}

Above code fails to compile because compiler confuses the declaration of objectB. A C++ compiler will not consider it as defining a variable named objectB of type ExampleClass calling a constructor that takes the object constructed by calling the ExampleClass constructor. Instead compiler will this think this line as a function named objectB which returns ExampleClass object and takes an unnamed function pointer that returns a ExampleClass object and does not take any parameters. Clang compiler lets you know of potential disambiguation.

main.cpp:14:20:warning: parentheses were disambiguated as a function
declaration [-Wve xing -parse]
MyClassobjectB(MyClass());
^~~~~~~~~~~
main.cpp:14:21:note: add a pair of parentheses to declare a variable
MyClassobjectB(MyClass());
^
( )

Weird ha!

Uniform Initialization

To fix this, C++11 has providedUniform Initialization.The code with uniform initialization will look like this.

classExampleClass{
private:
intm_Member;
public:
ExampleClass() =default;
MyClass(constMyClass& rhs) =default;
};

intmain()
{
ExampleClass objectA;
ExampleClass objectB{ ExampleClass{} };//<----- Notice this initialization.
return0;
}

Boom! Compiler is no more confused.

Narrowing Conversion

Uniform initializationalso prevent narrowing conversion. Lets take this example.

#include<iostream>

intmain()
{
intanInt{100};
charaChar{512};//--> Danger Will Robinson!
doubleaDouble{1.0};
floataFloat{ aDouble };//--> Error! Abort!
return0;
}

This code will not compile, and compiler will throw bunch of errors. If we look carefully at the lines with comments above we can see that we are implicitly trying to do narrowing conversion. A char type can store a maximum value of 255, therefore the value 512 would be narrowed into this data type. A C++11 or newer compiler will not compile this code. Similarly, the initialization of the float from a double type is also a narrowing conversion. To overcome this we usestatic_castto let compiler know that narrowing conversion are intentional.

#include<iostream>

intmain()
{
intanInt{100};
charaChar{static_cast<char>(512) };
doubleaDouble{1.0};
floataFloat{static_cast<float>(aDouble) };
return0;
}

This will be compiled easily.

Concept 2: Initializer Lists (code)

Initialization lists are used when you want to construct objects from multiple objects of a given type. C++11 provides initialization lists for this. We can supply many objects of same time together to a constructor. Initializer list are built uponuniform initialization.Let's see following vector example.

#include<iostream>
#include<vector>

intmain()
{
std::vector<int>vectorA(1);
std::cout <<"vectorA size:"<< vectorA.size()
<<"vectorA[0]:"<< vectorA[0] << std::endl;
std::vector<int>vectorB(1,10);
std::cout <<"vectorB size:"<< vectorB.size()
<<"vectorB[0]:"<< vectorB[0] << std::endl;
return0;
}

As we are used to initialize various objects in C++, example

intmyInt(12);

HeremyIntwill hold value 12, howevervectorAvariable will be initialized with a single integer containing 0. LikemyInt,we might expect it would contain a single integer containing 1 but that would be incorrect.The first parameter to vector constructor determines the how many values initial vector would be storing.In this case, we are asking it to store 1 value. Similarly,vectorB(1, 10)is declaration of vector with size 1 andvectorB[0]will be 10. Parameter 2 specifies a value to use to instantiate the members of the vector rather than using the default value.

Hmm, so how do we initialize a vector with multiple values. For that, we use initialization-lists.

std::vector<int> vectorC{1,10}

vectorChere is initialized usinguniform initialization.vectorCwill be of size 2 andvectorC[0]would be 1, andvectorC[1]would be 10. We could have also used explicit initialization list to initialize. Example:

std::initializer_list<int> aList{1,10};
std::vector<int>vectorD( aList );//or std::vector<int> vectorD{ aList };

The vector constructor implicitly creates a initializer_list when we initializedvectorCusing uniform initialization.

Concept 3: Deducing type of the object. (code)

Modern C++ provides type deduction for determining the type of objects. It providesautokeyword andtypeidmethod to determine types. Example.

#include<iostream>

intmain()
{
autovariable =5.5;
std::cout <<"Type of Variable:"<<typeid(variable).name() << std::endl;
return0;
}

We will see output of above program ( on clang )

Type of Variable: d

dhere is clang's internal representation ofdoubletype. If you pass the above output ofc++filtyou will see the output as

Ravis-MacBook-Pro:type-deduction mandliya$./run | c++filt -t
Type of Variable: double

So what happened here.autokeyword based on the initializing object determined that variable is of type double. (Since we initialized variable with 5.5), andtypeidprovided us the internal type compiler assigned to variable which is than later translated byc++filt.

auto for user created object types.

Let's useautofor user created objects.

#include<iostream>
#include<typeinfo>

classExampleClass{ };

intmain()
{
autovar =ExampleClass();
std::cout <<"Type of Variable:"<<typeid(var).name() << std::endl;
return0;
}

Here the var is of typeExampleClassand usingc++filtwe get exactly that

Ravis-MacBook-Pro:type-deduction mandliya$./run | c++filt -t
Type of Variable: ExampleClass

auto with initializer list (code)

In previous compiler versions, If our object is initialized using initializer list (or to say usinguniform initialization), then auto won't be that helpful. For example

autovar1{4.5};
autovar2{ ExampleClass{ } };

used to give output of

std::cout <<"Type of Variable:"<<typeid(var1).name() << std::endl;
std::cout <<"Type of Variable:"<<typeid(var2).name() << std::endl;

as

Ravis-MacBook-Pro:type-deduction mandliya$./run | c++filt -t
Type of Variable: std::initializer_list<double>
Type of Variable: std::initializer_list<ExampleClass>

However, after feedback, this is now changed to:

Ravis-MBP:type-deduction mandliya$./run | c++filt -t
Type of Variable:double
Type of Variable: ExampleClass

so for more than one elements, auto will detect them as appropriate initializer list.

autoint_initializer_list = {1,2,3};
autodouble_initiazer_list = {1.4,1.2,4.5};
std::cout <<"Type of the list:"<<typeid(int_initializer_list).name() << std::endl;
std::cout <<"Type of the list:"<<typeid(double_initializer_list).name() << std::endl;

Their types are deduced to:

Type of the list:std::initializer_list<int>
Type of the list:std::initializer_list<double>

This is error as compiler will not be able to determine the type.

autoerror_initializer_list = {4.5,5,2.4f};

Complete example isherewhose output in clang using c++filt

Ravis-MBP:type-deduction mandliya$./run | c++filt -t
Type of Variable:double
Type of Variable: ExampleClass
Type of the list:std::initializer_list<int>
Type of the vector:std::__1::vector<int,std::__1::allocator<int> >
Printingintvector created withautodeduced initializer list:123
Type of the list:std::initializer_list<double>

auto with functions

Below code snippet will only work inC++14and later.

#include<iostream>

template<typenameT>
autodoSomething( T parameter )
{
returnparameter;
}

intmain()
{
std::cout <<"Type of Variable:"<<typeid(doSomething(1.5)).name() << std::endl;
return0;
}

doSomethingreturns a double type in this call. In C++11 we will get this error.

Ravis-MacBook-Pro:type-deduction mandliya$ g++ -Wall -pedantic -Wextra -o run -std=c++11 auto-with-functions.cpp
auto-with-functions.cpp:9:1: error: 'auto' return without trailing return type;
deduced return types are a C++14 extension
auto doSomething( double parameter )

auto with function and trailing return-type

Another way to determine type of returning value of a function is usingdecltype.This method will work with C++11 and later.

template<typenameT>
autodoSomethingAgain( T parameter )->decltype(parameter)
{
returnparameter;
}

Here we are using atrailing return typeto decide return type of object.decltypeis used to tell the compiler to use the type of a given expression. The expression can be a variable name however we could also give a function here and decltype would deduce the type returned from the function.

Concept 4: Move semantics (code)

Move semantics is one of the most powerful features introduced in C++11. Consider this code example.

Tfunc(T o) {
//do something with o
returno
}

This function is using concept ofcall by value.When this function is called, an object copy of 'o' of type T is constructed. And since it also returns an object of type T, another copy of 'o' is constructed for the return value. Thus for a call offunctwo new objects of type T are constructed. First one is temporary object which is destroyed as we return. (Objects are destroyed when control flow moves out of scope in which object was created.)

What if, instead of creating new temporary object, we move data from one object to other directly. Let us understand first what arervaluesandlvaluesin C++. Traditionally, any expression thatcan appearon the left side of an assignment is termed aslvalue.Likewise, an expression thatmay onlyappear on the right side is calledrvalue.Note again that lvaluecan appearon the left side of an assignment. It may also appear on the right side, but it's still an lvalue because it would be legal for it to appear on the left. That is how both lvalues and rvalues got their name. Specific to move semantics, we are discussing here, an rvalue can bemoved.

a = b//a is lvalue and b is rvalue
a = b + c;
//^ A nameless value

A temporary value that is ready to expire, it is also called anxvalueor anexpiringvalue. A literal value is sometimes calledpure rvalueorprvalue.

a = 42;
// a is lvalue, 42 is prvalue.

Typicallyprvaluecategory includes literal values and anything returned from a function that is not a reference. The one thing all of these type categories have in common is that they can be moved.

The C++ library provides a template function called move. It's used to tell the compiler to use an object as if it were an rvalue, which means that it makes it moveable. Let's see an example.

#include<iostream>
#include<vector>

voidprintVec(std::vector<std::string> vec) {
std::cout <<"Vector size:"<< vec.size() << std::endl;
for(auto& str: vec ) {
std::cout << str <<"";
}
std::cout << std::endl;
}


intmain(intargc,constchar* argv[]) {

std::vector<std::string> vec1{"One","Two","Three","Four","Five"};
std::vector<std::string> vec2{"Six","Seven","Eight","Nine","Ten"};

//before move
printVec(vec1);
printVec(vec2);

vec2 =std::move(vec1);
std::cout <<"Using std::move"<< std::endl;

//before move
printVec(vec1);
printVec(vec2);


return0;
}

Following is the output of above program

Vector size:5
One Two Three Four Five
Vector size:5
Six Seven Eight Nine Ten


Using std::move

Vector size:0

Vector size:5
One Two Three Four Five

Clearly, contents ofvec1are moved tovec2andvec1has become empty.std::moveexpects an argument which is castable to rvalue. Here, vector is castable. The other side of the equals sign, the thing that it's being moved to must support a move copy and in this case, vec2 does.

Now, let's take another example, lets create our own swap function using std::move.

template<typenameT>
voidswap(T & a, T & b)
{
Ttmp(std::move(a));
a(std::move(b));
b(std::move(tmp));
}

Instead of callingvec2 = std::move(vec1)in above program, if we callswap(vec1, vec2),The output would be like this

Vector size:5
One Two Three Four Five
Vector size:5
Six Seven Eight Nine Ten


Using std::move

Vector size:5
Six Seven Eight Nine Ten
Vector size:5
One Two Three Four Five

See, the contents of two vectors are swapped.Tmpin swap is destroyed once we move out of scope, however, in the swap, we did not actually copy anything, we just moved things around.

We will get back to move semantics again.

Concept 5: Easier nested namespace syntax (C++17) (code))

The usual way of nested namespace syntax till C++14 is:

namespacegame{
namespaceengine{
namespaceplayer{
namespaceaction{
intmove;
}
}
}
}

For a larger application, this could get a little cumbersome. In c++ 17, the easier nested namespace syntax is introduced. So the above nested namespace could be declared as:

namespacegame::engine::player::action {
intmove;
}

Complete example could be foundhere.

Concept 6: Static Assert at compile time. (code)

C++11 introduced static assert for compile time check. This helps in doing compile time checks to avoid building linking and then running to eventually fail some assumptions about the piece of code, or compiler behavior etc. For example checking for a version of a library which is declared as static const.

staticconstintmajor_version =2;

Say version 3 of the library has new functions, and you need them for your project but you downloaded version 2 project. Now you compile your project,and the lib project link it and then eventually fail after 30 minutes.

Instead the below check would fail at compile time.

static_assert(lib::major_version >2,"You are using version less than 2, which might not have newer functionalities");

C++11 had a compulsory requirement for second argument, as string literal message, however C++17 has relaxed this requirement and made it optional. Now if you don't provide the argument, compiler will print a default message.

constintx =3;
constinty =4;
static_assert(x == y,"x should be equal to y); // This will not fail.
static_assert(x > y,"x should be greater than y"); // This will fail at compile time.

// This will work in C++17 but not in C++11 or C++14.
static_assert(x < y);

Complete code example ishere.The compile time errors look like this.

Ravis-MBP:static_assert mandliya$ g++ -Wall -Wpedantic -o run -std=c++1z static_assert.cpp
static_assert.cpp:12:5:error: static_assert failed"x should be greater than y"
static_assert(x > y,"x should be greater than y");
^ ~~~~~
static_assert.cpp:15:5:error: static_assert failed
static_assert(x < y);
^ ~~~~~
2errors generated.

Concept 7: std::invoke

std::invoke is used to call functions, function pointers, and member pointers with the same syntax. This is generally called to anyCallableobject with/out parameters.

Example:

voidprint_num(inti)
{
std::cout << i <<'\n';
}

.
.
.
std::invoke(print_num,10);

Unnamed namespaces

Modern C++ supports unknown namespaces.

Releases

No releases published

Packages

No packages published

Languages