Introduction
We have covered few features in the first part "C++11 – A Glance [part 1 of n]". In this part, let's glance at the following features:
Feature | Intent | VS2010 status |
Strongly typed enums | Type safety improvement
| no
|
Rvalue references | Performance improvement
| yes
|
Move semantics and Perfect
forwarding | Performance improvement
| yes
|
long long | Performance improvement
| yes
|
Override control | Usability improvement
| yes
|
preventing narrowing | Performance improvement
| no
|
Range based for-loops | Usability & Performance improvement
| no
|
As quoted by Stroustrup “C enumerations constitute a curiously half-baked concept” and very few modifications are done to rectify their shortfalls resulting in manifestation of silent behavioral changes. Let's check an example to support this:
namespace DefenceNetwork
{
namespace WeatherMonitor
{
enum CycloneWarningLevels
{
GREY, RED, YELLOW, GREEN, BLUE };
}
namespace ThreatMonitor
{
enum AlertConditions
{
BLUE, GREEN, YELLOW, RED, GREY };
}
}
using namespace DefenceNetwork;
void SetDEFCONLevel(int value)
{
using namespace WeatherMonitor;
if(value >= RED)
{
cout<<"Nuclear war is imminent...All Missiles GO...GO...GO"<<endl;
}
}
void main()
{
using namespace ThreatMonitor;
SetDEFCONLevel( AlertConditions::GREEN );
}
The problem with the enum
s so far is that:
- They can be silently converted to
int
.
In the above example, SetDEFCONLevel( )
method needs int
and when we pass an enumerator, it happily accepted. - The enumerators of the
enum
are exported to the scope in which enum
is defined, thus causing name clashes and surprises. In the above case, see the surprises yourself. - They have an implementation-defined underlying type and their type cannot be specified by the developer leading to confusion, compatibility issues, etc.
Let's visualize this via another example:
Take a case where we want to check if a number is present between certain pre-defined intervals. As we have fixed intervals, we can go for an enum
to define them.
enum INTERVALS
{
INTERVAL_1 = 10,
INTERVAL_2 = 100,
INTERVAL_3 = 1000,
INTERVAL_4 = 0xFFFFFFFFU };
unsigned long longValue = 0xFFFFFFFFU; INTERVALS enumMyInterval = (INTERVALS)0xFFFFFFFFU;
if( enumMyInterval > longValue)
{
cout<<"Invalid interval - Out of range"<<endl;
}
I bet the validation in the above case will not be true
as enumMyInterval
will never be treated as unsigned int
(4294967295
) and will be -1
. And there is no way to make INTERVAL_4
point to unsigned int
as it will default to int
.
Keeping all these in mind, in C++11, we got what is called enum
class - a strongly typed enum
.
NOTE: VS2010 does not support this feature... hope they will support in VS2011.
Let us quickly glance the new syntax:
- With C++11, the enumerators are no longer exported to surrounding scope and requires scope identifer:
enum class CycloneWarningLevels { GREY, RED, YELLOW, GREEN, BLUE };
- The underlying type by default is
int
, but C++11 gave an option to specify the type:
enum class INTERVALS : unsigned long
{
INTERVAL_1 = 10, INTERVAL_2 = 100, INTERVAL_3 = 1000, INTERVAL_4 = 0xFFFFFFFFU
}
- Enumerators are strongly-typed and no longer implicitly convertable to
int
.
SetDEFCONLevel( AlertConditions::GREEN );
- Forward declarations are now possible:
enum class INTERVALS : unsigned long; void foo(INTERVALS* IntervalsEnum_in) { }
If you are not familiar with Lvalue
s and Rvalue
s, please have a glance at "The Notion of Lvalues and Rvalues". It will surely help you in understanding this feature.
To handle certain scenarios, the C++ compilers silently create at times temporaries that can seriously hit the performance of the code. With evolution of compilers, few of these temporary creations are arrested but many more slipped leading to relatively in-efficent programs. Let's see what I am saying:
vector<double> GetNDimensionalVector()
{
vector<double> vecTransMatrix;
vecTransMatrix.push_back(10.5);
vecTransMatrix.push_back(1.3);
return vecTransMatrix;
}
int _tmain(int argc, _TCHAR* argv[])
{
vector<double> vecNewMatrix = GetNDimensionalVector();
size_t size = vecNewMatrix.size();
}
If we analyze this code, GetNDimensionalVector( )
method created a vector say of 10 doubles which will require 10*sizeof(double)
bytes. Now while returning a compiler (prior to VS03 for example) will again create a copy. However, recent compilers fixed this hole (via Return value optimization - aka RVO). Now the call to GetNDimensionalVector( )
will copy all its content again to vecNewMatrix
(upon which further operations are done) and the result of GetNDimensionalVector( )
call is evaporated as after ; as this is temporary. What a pity!, we are wasting a lot of memory chunks instead of just pilfering the data inside the temporary vector into vecNewMatrix
.
A smart language should allow this. And this is exactly what they have provided us through Rvalue
reference.
The '&&
' is the token that identifies the reference as an "rvalue
reference" and distinguishes it from the current (lvalue
) reference '&
'.
Let's see a function with Rvalue
reference:
void PrintData(string& str) { } void PrintData(string&& str) { }
string str="Hellow C++11 world"; PrintData(str); PrintData( "Hellow C++11 world" );
This feature resulted in the possibility of supporting 'Move semantics' and 'Perfect forwarding'.
The implementation of move semantics significantly increases the performance as the resources of temporary object (that cannot be referenced elsewhere in the program as it is going to evaporate) can be pilfered instead of copying.
To get a better understanding, take the case when a vector needs more capacity and if no continuous memory is available. Then it will identify a memory location which is large enough to hold its old contents plus required (new) capacity. It will then copy all the old contents to this new location. Now, this call to copy constructor is expensive if the contents are a 'string
' or a heavy-duty class/structure. The pity here is that all the old location contents will be evaporated. How nice it would be if this operation involves just stealing the old contents instead of copying.
Hope you got what I say.
Please note that the copy operation leaves the source unchanged while a move operation on the other hand leaves the source either unchanged or it may be radically different. Now if a developer chooses Move operation upon an object, then he should no more care about the state of the source object [he should keep in mind that the source object's state is disturbed and is not more useful].
If his intention is still to use source along with duplicate, then he should be doing copying (as is done till now) and not move.
Before going to the implementation part, just check these points:
- To implement Move semantics, we typically provide a MOVE constructor and (an optional) MOVE assignment operator.
- The compiler won't provide us with default Move constructor, if we don't provide one.
- And declaring a Move constructor will stop the compiler to generate default constructor.
Now let's go to the implementation part:
class MyFileStream
{
unsigned char* m_uchBuffer;
unsigned int m_uiLength;
public:
MyFileStream(unsigned int Len) : m_uiLength(Len), m_uchBuffer(new unsigned char[Len]) {}
MyFileStream(const MyFileStream& FileStream_in) { }
MyFileStream& operator =(const MyFileStream& FileStream_in)
{
return *this;
}
MyFileStream(MyFileStream&& FileStream_in) : m_uiLength( FileStream_in.m_uiLength ),
m_uchBuffer( FileStream_in.m_uchBuffer )
{
FileStream_in.m_uiLength = 0;
FileStream_in.m_uchBuffer = NULL;
}
MyFileStream& operator =(MyFileStream&& FileStream_in)
{
if( this != &FileStream_in)
{
delete [] m_uchBuffer;
m_uiLength = FileStream_in.m_uiLength;
m_uchBuffer = FileStream_in.m_uchBuffer;
FileStream_in.m_uiLength = 0; FileStream_in.m_uchBuffer = NULL;
}
return *this;
}
~MyFileStream()
{
if( NULL != m_uchBuffer) delete [] m_uchBuffer;
m_uchBuffer = NULL;
}
};
MyFileStream GetMyStream(MyFileStream FileStream) {
return FileStream;
}
int _tmain(int argc, _TCHAR* argv[])
{
MyFileStream objMyStream(100);
MyFileStream objMyStream2 = GetMyStream( objMyStream );
}
The comments in the sample code above are pretty much self explanatory. Just note that for Move constructor and assignment operator, we took the argument without const
, as we intent to modify them (we want to set them to default once we moved their content to target).
There are many more points that need to be grasped in this feature but those are out-of-scope on this introductory part. I will just cover one more scenario and wind up this section.
In the above example, MyFileStream
class has default member types. What if the members are of some other class type, say like MyString
.
class MyString
{
public:
MyString(){}
MyString(const MyString& String_in){ }
MyString(MyString&& String_in){ }
MyString& operator=(const MyString& String_in){ return *this; }
Move Assigment operator
MyString& operator=(MyString&& String_in){ return *this; }
};
And our MyFileStream
class has this as a member:
class MyFileStream
{
unsigned char* m_uchBuffer;
unsigned int m_uiLength;
MyString m_strFileName;
};
Now how to steal this MyString
object data efficiently or if I re-phrase it How to fit this class object into our move culture.
Will a call to MyString
constructor from MyFileStream
's move constructor automatically call MyString MOVE
constructor. Of course NO.
Can you get why not? It's simple - this call will pass MyString
object as lvalue
and hence its copy constructor is called.
So what is the work-around? Simple. Convert this Lvalue
to Rvalue
!!! Bingo.
Now how to do this?
We can convert an Lvalue
to Rvalue
by using static_cast
.
m_strFileName = static_cast<MyString&&>( FileStream_in.m_strFileName );
Or another way is to use std::move
(again a new STL method provided in C++11).
m_strFileName = std::move( FileStream_in.m_strFileName );
MyFileStream(MyFileStream&& FileStream_in) : m_uiLength( FileStream_in.m_uiLength ),
m_uchBuffer( FileStream_in.m_uchBuffer ),
m_strFileName( std::move( FileStream_in.m_strFileName) )
{
FileStream_in.m_uiLength = 0;
FileStream_in.m_uchBuffer = NULL;
}
Perfect forwarding: Another nice effect of Rvalue
implementation is the solution to the Forwarding problem.
Before going any further, let's grasp this forwarding problem.
Suppose we have two structures handling Licence operations, one OpenLicence
and another ClosedLicence
and say suppose if we want to do some master check before creating the object for either of these structures, then we can use a wrapper template function where we can do this master check and then simply pass (forward) the arguments to the structures.
struct OpenLicence
{
OpenLicence(int& Key1, int& Key2){}
};
struct ClosedLicence
{
ClosedLicence(int& Key1, int& Key2){}
};
template<typename T, typename X, typename Y>
T* Licence_Wrapper(X& x, Y& y)
{
return new T(x, y);
}
void main()
{
int key1 = 232; int key2 = 007;
Licence_Wrapper<OpenLicence>( key1, key2 );
Licence_Wrapper<OpenLicence>( key1, 007 ); }
Now to solve this, we have to overload our wrapper function and also our structures:
struct OpenLicence
{
OpenLicence(int& Key1, int& Key2){}
OpenLicence(int& Key1, const int& Key2){} };
struct ClosedLicence
{
ClosedLicence(int& Key1, int& Key2){}
ClosedLicence(int& Key1, const int& Key2){} };
template<typename T, typename X, typename Y> T* Licence_Wrapper(X& x, Y& y)
{
return new T(x, y);
}
template<typename T, typename X, typename Y> T* Licence_Wrapper(X& x, const Y& y) {
return new T(x, y);
}
Now what if the first argument is an Rvalue
(Licence_Wrapper
<openlicence>( 007, key2 )
) or what if both are Rvalue
s ( Licence_Wrapper
<openlicence>( 006, 007 );
)
To handle these, we should have that many overloads. More number of arguments leads to more number of overloads. Our code will be pumped with overloads to handle all these... Welcome to the forwarding problem.
Rvalue
references just solve this in one stroke:
template<typename T, typename X, typename Y>
T* Licence_Wrapper(X&& x, Y&& y)
{
return new T(x, y);
}
That's it! No more overloads needed anywhere. This is called PERFECT FORWARDING. Really perfect, isn't it. There are many more to discuss in this topic, but again as this in an introductory article, I won't cover them here.
long long
is a 64-bit integer type. Previous to C++11, the largest integer type was long
and its size was platform (32 or 64) dependent. But this long long
guarantees to be atleast 64-bit long. Actually, this concept is accepted in C++99 and as many compilers already supported it, the C++11 committee gave a thumbs up for this new integral type.
Say, suppose we have a base
class with a virtual
function. In any of its derived classes, this function can be overridden and no special keyword or annotation is needed upon this function to do so. To put more clarity and to say that we are overriding a base class function, C++11 introduced a new keyword called override
. A declaration marked 'override
' is only valid if there is a function to override. This feature is shipped into VS2010. Let's see an example:
class Base
{
public:
virtual void Draw(){}
void SomeFunction(){}
};
class Derived : public Base
{
public:
void Draw() override {}
void SomeFunction() override {} };
void main()
{
int pi = {3.14};
}
To prevent this type of undesired conversions, the C++11 defined that {}
initialization will not allow truncation or narrowing. As per this:
void main()
{
int pi = {3.14}; int i{5.112}; }
Even a conversion from 3.0
to 3
is also considered as narrowing and an error is given with {}
initialization. This feature too is omitted from VS2010.
Before 'auto
' to iterate an vector requires typing lot of code:
for(vector<int>::iterator itr = vec.begin(); itr != vec.end(); itr++) { }
But after 'auto
' life became easy:
for(auto itr = vec.begin(); itr != vec.end(); itr++) { }
C++11 still simplified these type of parsings by providing what is called Range-for support. Now all we have to do is:
for( auto val : vec ) { cout <<val<<endl; }
This is like calling for each value of val
in vec
from begin to end.
This feature works for any C-style arrays and for all those which support iteration via begin and end functions. This feature is also omitted in VS2010.
Rest of the features will be covered in the next part.
Thank you for reading this article. It would be helpful if you rate/send feedback, so that I can improve while working on the remaining parts or updating this part with new information.
Acknowledgments
Thanks again to Clement Emerson for his views and review.
Other Sources
History
- 13th January, 2011: Added Part 2 as a continuation to "C++11 – A Glance [part 1 of n]"
- 21st January, 2012: Corrected few broken links [no additional information]