C++ Recipes: A Problem-Solution Approach (2015)
CHAPTER 2
Modern C++
Development of the C++ programming language began in 1979 as the C with Classes language. The name C++ was formally adopted in 1983 and development of the language continued throughout the 1980s and 1990s without the adoption of a formal language standard. This all changed in 1998 when the first ISO standard of the C++ programming language was adopted. There have been three updates to the standard published since that time, one in 2003, again in 2011 and the latest in 2014.
Note The standard published in 2003 was a minor update to the 1998 standard that didn’t introduce much in the way of new features. For this reason, it won’t be discussed in any great detail in this book.
This book is primarily going to focus on the very latest C++ programming standard, C++14. Whenever I mention the C++ programming language you can be assured that I am talking about the language as described by the current ISO standard. If I am discussing features that were introduced in 2011 then I will explicitly mention the language as C++11 and for any features that were introduced prior to 2011 I will use the name C++98.
This chapter will look at the programming features added to the language in the latest standard and with C++11. Many of the modern features of C++ were added in the C++11 standard and have been expanded with the C++14 standard therefore it is important to be able to identify the differences when working with compilers that support a standard that is not the latest.
Recipe 2-1. Initializing Variables
Problem
You would like to be able to initialize all variables in a standard manner.
Solution
Uniform initialization was introduced in C++11 and can be used to initialize a variable of any type.
How It Works
It’s necessary to understand the deficiencies with variable initialization in C++98 to appreciate why uniform initialization is an important language feature in C++11. Listing 2-1 shows a program that contains a single class, MyClass.
Listing 2-1. The C++Most Vexing Parse Problem
class MyClass
{
private:
int m_Member;
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
};
int main()
{
MyClass objectA;
MyClass objectB(MyClass());
return 0;
}
The code in Listing 2-1 will generate a compile error in C++ programs. The problem exists in the definition of objectB. A C++ compiler will not see this line as defining a variable named objectB of type MyClass calling a constructor that takes the object constructed by calling theMyClass constructor. This is what you might expect the compiler to see however what it actually sees is a function declaration. The compiler thinks that this line is declaring a function named objectB that returns a MyClass object and has a single, unnamed function pointer to a function that returns a MyClass object and is passed no parameters.
Compiling the program shown in Listing 2-1 causes Clang to generate the following warning:
main.cpp:14:20: warning: parentheses were disambiguated as a function
declaration [-Wvexing-parse]
MyClass objectB(MyClass());
^~~~~~~~~~~
main.cpp:14:21: note: add a pair of parentheses to declare a variable
MyClass objectB(MyClass());
^
( )
The Clang compiler has properly identified that the code entered in Listing 2-1 contains a vexing parse problem and even helpfully suggests wrapping the MyClass constructor being passed as a parameter in another pair of parentheses to solve the problem. C++11 has provided an alternative solution in uniform initialization. You can see this in Listing 2-2.
Listing 2-2. Using Uniform Initialization to Solve the Vexing Parse Problem
class MyClass
{
private:
int m_Member;
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
};
int main()
{
MyClass objectA;
MyClass objectB{MyClass{}};
return 0;
}
You can see in Listing 2-2 that uniform initialization replaces parentheses with braces. This syntax change informs the compiler that you would like to use uniform initialization to initialize your variable. Uniform initialization can be used to initialize almost all types of variables.
Note The paragraph above mentions that uniform initialization can be used to initialize almost all variables. It can have trouble when initializing aggregates or plain old data types however you won’t need to worry about those for now.
The ability to prevent narrowing conversions is another benefit of using uniform initialization. The code in Listing 2-3 will fail to compile when using uniform initialization.
Listing 2-3. Using Uniform Initialization to Prevent Narrowing Conversions
int main()
{
int number{ 0 };
char another{ 512 };
double bigNumber{ 1.0 };
float littleNumber{ bigNumber };
return 0;
}
The compiler will throw errors when compiling the code in Listing 2-3 as there are two narrowing conversions present in the source. The first occurs when trying to define a char variable with the literal value 512. 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 due to this error. The initialization of the float from a double type is also a narrowing conversion. Narrowing conversions occur when data is transferred from one type to another in where the destination type cannot store all of the values represented by the source type. Precision is lost in the case of a double being converted to a float therefore the compiler correctly will not build this code as-is. The code in Listing 2-4 uses a static_cast to inform the compiler that the narrowing conversions are intentional and to compile the code.
Listing 2-4. Using a static_cast to Compile Narrowing Conversions
int main()
{
int number{ 0 };
char another{ static_cast<char>(512) };
double bigNumber{ 1.0 };
float littleNumber{ static_cast<float>(bigNumber) };
return 0;
}
Recipe 2-2. Initializing Objects with Initializer Lists
Problem
You would like to construct objects from multiple objects of a given type.
Solution
Modern C++ provides initializer lists that can be used to supply many objects of the same type to a constructor.
How It Works
Initializer lists in C++11 build upon uniform initialization to allow you to initialize complex types with ease. A common example of a complex type that can be difficult to initialize with data is a vector. Listing 2-5 shows two different calls to a standard vector constructor.
Listing 2-5. Constructing vector Objects
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
return 0;
}
The code in Listing 2-5 might not do what you expect at first glance. The vectorA variable will be initialized with a single int containing 0. You might expect that it would contain a single integer containing 1 but this would be incorrect. The first parameter to a vector constructor determines how many values the initial vector will be set up to store and in this case we are asking it to store a single variable. You might similarly expect vectorB to contain two values, 1 and 10 however what we have here is a vector that contains one value and that value is 10. ThevectorB variable is constructed using the same constructor as vectorA however it specifies a value to use to instantiate the members of the vector rather than using the default value.
The code in Listing 2-6 uses an initializer list in conjunction with uniform initialization to construct a vector that contains two elements with the specified values.
Listing 2-6. Using Uniform Initialization to Construct a vector
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
MyVector vectorC{ 1, 10 };
cout << vectorC.size() << " " << vectorC[0] << endl;
return 0;
}
The code in Listing 2-6 creates three different vector objects. You can see the output generated by this program in Figure 2-1.
Figure 2-1. The Output Generated by Listing 2-6
The console output shown in Figure 2-1 shows the size of each vector and the value stored in the first element of each vector. You can see that the first vector contains a single element and that its value is 0. The second vector also contains a single element however its value is 10. The third vector is constructed using uniform initialization and it contains two values and the value of its first element is 1. The value of the second element will be 10. This can cause a significant different to the behavior of your programs if you are not taking particular care to ensure that the correct type of initialization has been used with your types. The code in Listing 2-7 shows a more explicit use of the initializer_list to construct a vector.
Listing 2-7. Explicit initializer_list Usage
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
initializer_list<int> initList{ 1, 10 };
MyVector vectorC(initList);
cout << vectorC.size() << " " << vectorC[0] << endl;
return 0;
}
The code in Listing 2-7 contains an explicit initializer_list that is used to construct a vector. The code in Listing 2-6 implicitly created this object when constructing a vector using uniform initialization. There’s usually little need to explicitly create initializer lists like this however it’s important that you understand what the compiler is doing when you write code using uniform initialization.
Recipe 2-3. Using Type Deduction
Problem
You would like to write portable code that doesn’t have a high maintenance cost when changing types.
Solution
C++ provides the auto keyword that can be used to let the compiler deduce the type for a variable automatically.
How It Works
C++98 compilers had the ability to automatically deduce the type of a variable however this functionality was only available while you were writing code that used templates and you omitted the type specialization. Modern C++ has extended this type deduction support to many more scenarios. The code in Listing 2-8 shows the use of the auto keyword and the typeid method of working out the type of a variable.
Listing 2-8. Using the auto Keyword
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
auto variable = 1;
cout << "Type of variable: " << typeid(variable).name() << endl;
return 0;
}
The code in Listing 2-8 shows how to create a variable with automatically deduced type in C++. The compiler will automatically work out that you wanted to create an int variable with this code and that’s the type that will be output by the program, sort of. The Clang compiler will output its internal representation of an integer type which is actually i. You can pass this output to a program named c++filt to convert this into a normal typename. Figure 2-2 shows how this can be achieved.
Figure 2-2. Using c++filt to Produce Proper Type Output From Clang
The c++filt program has successfully converted the Clang type i into a human readable C++ type format. The auto keyword also works with classes. Listing 2-9 shows this.
Listing 2-9. Using auto with a class
#include <iostream>
#include <typeinfo>
using namespace std;
class MyClass
{
};
int main()
{
auto variable = MyClass();
cout << "Type of variable: " << typeid(variable).name() << endl;
return 0;
}
This program will print out the name MyClass as you can see in Figure 2-3.
Figure 2-3. Using auto with MyClass
Unfortunately there are times where the auto keyword can produce less than desirable results. You will definitely come unstuck if you try to combine the auto keyword with uniform initialization. Listing 2-10 shows the use of the auto keyword with uniform initialization.
Listing 2-10. Using auto with Uniform Initialization
#include <iostream>
#include <typeinfo>
using namespace std;
class MyClass
{
};
int main()
{
auto variable{ 1 };
cout << "Type of variable: " << typeid(variable).name() << endl;
auto variable2{ MyClass{} };
cout << "Type of variable: " << typeid(variable2).name() << endl;
return 0;
}
You might expect that the code in Listing 2-10 will produce a variable of type int and a variable of type MyClass however this is not the case. Figure 2-4 shows the output generated by the program.
Figure 2-4. Output Generated When using auto with Uniform Initialization
A quick look at Figure 2-4 shows the immediate problem encountered when using the auto keyword along with uniform initialization. The C++ uniform initialization feature automatically creates an initializer_list variable that contains the value of the type we want, not the type and value itself. This leads to a relatively simple piece of advice, do not use uniform initialization when defining variables using auto. I’d recommend not using auto even if the type you want is actually an initializer_list as the code is much easier to understand and much less error prone if you don’t mix and match you variable initialization styles. There’s a final piece of advice to bear in mind, use auto for local variables as much as possible. It’s impossible to declare an auto variable and not define it therefore it’s impossible to have an undefined local autovariable. You can use this piece of knowledge to cut down on one potential source of bugs in your programs.
Recipe 2-4. Using auto with Functions
Problem
You would like to create more generic functions using type deduction to increase code maintainability.
Solution
Modern C++ allows you to use type deduction for function parameters and for return types.
How It Works
C++ allows you to utilize type deduction when working with function using two methods. Types can be deduced for function parameters by creating a template function and calling that function without explicit specializers. The return type can be deduced for a function using the auto keyword in place of its return type. Listing 2-11 shows the use of auto to deduce the return type for a function.
Listing 2-11. Deducing a Function’s Return Type Using auto
#include <iostream>
using namespace std;
auto AutoFunctionFromReturn(int parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromReturn(1);
cout << value << endl;
return 0;
}
The AutoFunctionFromReturn function’s return type in Listing 2-11 is automatically deduced. The compiler inspects the type of the variable returned from the function and uses that to deduce the type to be returned. This all works properly because the compiler has everything it needs inside the function to be able to deduce the type. The parameter variable is being returned therefore the compiler can use its type as the return type for the function.
Things get a bit more complicated when you need to build with a C++11 compiler. Building Listing 2-11 using C++11 results in the following error.
main.cpp:5:1: error: 'auto' return without trailing return type
auto AutoFunctionFromReturn(int parameter)
Listing 2-12 includes a function with automatic return type deduction that works in C++11.
Listing 2-12. Return Type Deduction in C++11
#include <iostream>
using namespace std;
auto AutoFunctionFromReturn(int parameter) -> int
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromReturn(1);
cout << value << endl;
return 0;
}
You might be wondering why you would bother doing this when looking at the code in Listing 2-12. There’s little use in deducing the return type for a function when you always specify that it will be an int and you’d be right. Return type deduction is much more useful in functions that don’t have their parameter types declared in their signature. Listing 2-13 shows the type deduction in action for a template function.
Listing 2-13. Deducing return types for C++11 template functions
#include <iostream>
using namespace std;
template <typename T>
auto AutoFunctionFromParameter(T parameter) -> decltype(parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromParameter(2);
cout << value << endl;
return 0;
}
Listing 2-13 shows a useful application of return type deduction. This time the function is specified as a template therefore the compiler cannot work out the return type using the parameter type. C++11 introduced the decltype keyword to compliment the auto keyword. decltype is used to tell the compiler to use the type of a given expression. The expression can be a variable name however you could also give a function here and decltype would deduce the type returned from the function.
At this point the code has come full circle. The C++11 standard allowed auto to be used on functions to deduce return type but required that the type still be specified as a trailing return type. The trailing return type can be deduced using decltype however this leads to overly verbose code. C++14 rectifies this situation by allowing auto to be used on functions without having the trailing return type even when used with templates as you can see in Listing 2-14.
Listing 2-14. Using auto to Deduce Return Type on a Template Function
#include <iostream>
using namespace std;
template <typename T>
auto AutoFunctionFromParameter(T parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromParameter(2);
cout << value << endl;
return 0;
}
Recipe 2-5. Working with Compile Time Constants
Problem
You would like to optimize the runtime operation of your program using compile time constant.
Solution
C++ provides the constexpr keyword that can be used to guarantee that an expression can be evaluated at compile time.
How It Works
The constexpr keyword can be used to create variables and functions that guarantee that their evaluation can be evaluated at compile time. Your compiler will throw an error if you add any code to them that prevents compile time evaluation. Listing 2-15 shows program that uses aconstexpr variable to define the size of an array.
Listing 2-15. Using constexpr to Define the Size of an array
#include <array>
#include <cstdint>
#include <iostream>
int main()
{
constexpr uint32_t ARRAY_SIZE{ 5 };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
The constexpr variable in Listing 2-15 guarantees that the value can be evaluated at compile time. This is necessary here as the size of an array is something that must be determined when your program is compiled. Listing 2-16 shows how you can extend this example to include aconstexpr function.
Listing 2-16. A constexpr Function
#include <array>
#include <cstdint>
#include <iostream>
constexpr uint32_t ArraySizeFunction(int parameter)
{
return parameter;
}
int main()
{
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(5) };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
You can go another step further than the code in Listing 2-16 and create a class with a constexpr constructor. This is shown in Listing 2-17.
Listing 2-17. Creating constexpr class Constructors
#include <array>
#include <cstdint>
#include <iostream>
class MyClass
{
private:
uint32_t m_Member;
public:
constexpr MyClass(uint32_t parameter)
: m_Member{parameter}
{
}
constexpr uint32_t GetValue() const
{
return m_Member;
}
};
int main()
{
constexpr uint32_t ARRAY_SIZE{ MyClass{ 5 }.GetValue() };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
The code in Listing 2-17 is able to create an object and call a method in a constexpr statement. This was possible because the constructor for MyClass was declared as a constexpr constructor. The code shown so far for constexpr has all been compatible with C++11 compilers. The C++14 standard has relaxed many of the restrictions that existed in C++11. C++11 constexpr statements are not permitted to do many things that normal C++ code can. Examples of these things are creating variables and using if statements. The code in Listing 2-18 shows a C++14constexpr function that can be used to limit the maximum size of an array.
Listing 2-18. Using a C++14 constexpr Function
#include <array>
#include <cstdint>
#include <iostream>
constexpr uint32_t ArraySizeFunction(uint32_t parameter)
{
uint32_t value{ parameter };
if (value > 10 )
{
value = 10;
}
return value;
}
int main()
{
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(15) };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
The code in Listing 2-18 expands on the C++11 compatible code in Listing 2-16 to include a function that declares a variable and uses an if statement. Compiling this code with a C++11 compiler results in the following error.
main.cpp:7:14: warning: variable declaration in a constexpr function is a C++1y extension [-Wc++1y-extensions]
uint32_t value{ parameter };
^
main.cpp:8:5: warning: use of this statement in a constexpr function is a C++1y extension [-Wc++1y-extensions]
if (value > 10 )
^
main.cpp:17:24: error: constexpr variable 'ARRAY_SIZE' must be initialized by a constant expression
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(15) };
Two warnings are presented to show that the constexpr function cannot be used in a constexpr context. This is not a compile error because the function can still be used in a non-constexpr context. The actual error is thrown when the function is used to initialize a constexprvariable.
Recipe 2-6. Working with Lambdas
Problem
You would like to write programs that utilize unnamed function objects.
Solution
C++ provides lambdas that can be used to create closures and can be passed around in your code.
How It Works
The lambda syntax introduced in C++11 can be a little confusing at first. Listing 2-19 shows a simple example of a program that uses a lambda to print out all of the values in an array.
Listing 2-19. Using a Lambda to Print array Values
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
int main()
{
using MyArray = std::array<uint32_t, 5>;
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.cbegin(),
myArray.cend(),
[](auto&& number) {
std::cout << number << std::endl;
});
return 0;
}
This code shows how a lambda is defined in C++ source code. The syntax for a lambda is as follows:
[] () {};
The braces represent the capture block. A lambda uses a capture block to capture existing variables to be used in the lambda. The code in Listing 2-19 does not have a need to capture any variables therefore it is empty. The parentheses represent the argument block as it does in a normal function. The lambda in Listing 2-19 has a single parameter that is of type auto&&. The std::for_each algorithm applies the given function to every element in the sequence. The function here happens to be a closure that was created by the compiler when it encountered the lambda syntax and passed it to the for_each function. There’s a subtle terminology difference there that you should become familiar with. A lambda is the source code construct that defines an anonymous or unnamed function. The compiler uses this syntax to create a closure object from the lambda.
A closure can be referenced by a variable as shown in Listing 2-20.
Listing 2-20. Referencing a Closure in a Variable
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <typeinfo>
int main()
{
using MyArray = std::array<uint32_t, 5>;
MyArray myArray{ 1, 2, 3, 4, 5 };
auto myClosure = [](auto&& number) {
std::cout << number << std::endl;
};
std::cout << typeid(myClosure).name() << std::endl;
std::for_each(myArray.begin(),
myArray.end(),
myClosure);
return 0;
}
The example in Listing 2-20 captures the lambda into an auto typed variable. Figure 2-5 shows the output that this generates.
Figure 2-5. The Type Output by typeid when Passed a Closure
Figure 2-5 shows the type of the closure stored by the myClosure variable in Listing 2-20. The automatically generated type here isn’t particularly useful however C++ does provide a method for passing around any type of object that can be called like a function. The functiontemplate is provided in the functional header and is part of the STL. This template takes the signature of the function that the object represents. You can see how this code looks in Listing 2-21.
Listing 2-21. Passing a Closure into a Function
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
using MyArray = std::array<uint32_t, 5>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
auto myClosure = [](auto&& number) {
std::cout << number << std::endl;
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
return 0;
}
You can now create closures and pass them around your program using the function template as shown in Listing 2-21. This allows you to add some touches to your programs that would have been much more difficult to achieve in C++98. Listing 2-22 shows a method to copy an array into a vector through a lambda using the capture block.
Listing 2-22. Using the Lambda Capture Feature
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = [&myCopy](auto&& number) {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
The code in Listing 2-22 contains a use of the lambda capture to store a reference to the object myCopy in the closure. This object can then be used inside the lambda and has each member of the array pushed onto it. The main function ends by printing all of the values stored by myCopyto show that the closure was sharing the same vector as main thanks to the reference capture. The capture was specified as a reference capture using the & operator. The vector would have been copied into the closure if this had been omitted and the myCopy vector in main would have remained empty.
Capturing myCopy by value rather than by reference would have led to another problem. The type the compiler creates for the lambda would no longer be a compatible argument with the parameter used to declare the function’s signature. Listing 2-23 shows the lambda using capture by value to copy myCopy.
Listing 2-23. Capturing myCopy by Value
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = [myCopy](auto&& number) {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
The code in Listing 2-23 won’t compile and your compiler is unlikely to give you a meaningful or helpful error message. Clang provides the following error output when trying to compile this code using Cygwin on Windows.
$ make
clang++ -g -std=c++1y main.cpp -o main
main.cpp:26:13: error: no matching member function for call to 'push_back'
myCopy.push_back(number);
~~~~~~~^~~~~~~~~
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2149:27: note: in instantiation of function template
specialization 'main()::<anonymous class>::operator()<unsigned int>' requested here
using _Invoke = decltype(__callable_functor(std::declval<_Functor&>())
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2158:2: note: in instantiation of template type alias
'_Invoke' requested here
using _Callable
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2225:30: note: in instantiation of template type alias
'_Callable' requested here
typename = _Requires<_Callable<_Functor>, void>>
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2226:2: note: in instantiation of default argument for
'function<<lambda at main.cpp:24:22> >' required here
function(_Functor);
^~~~~~~~
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2226:2: note: while substituting deduced template arguments
into function template 'function' [with _Functor = <lambda at main.cpp:24:22>, $1 = <no value>]
function(_Functor);
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_vector.h:913:7: note: candidate function not viable: 'this'
argument has type 'const MyVector' (aka 'const vector<MyArray::value_type>'), but method is not marked const
push_back(const value_type& __x)
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_vector.h:931:7: note: candidate function not viable: 'this'
argument has type 'const MyVector' (aka 'const vector<MyArray::value_type>'), but method is not marked const
push_back(value_type&& __x)
^
main.cpp:30:5: error: no matching function for call to 'PrintArray'
PrintArray(myClosure);
^~~~~~~~~~
main.cpp:12:6: note: candidate function not viable: no known conversion from '<lambda at main.cpp:24:22>' to 'const
std::function<void (MyArray::value_type)>' for 1st argument
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
^
2 errors generated.
makefile:2: recipe for target 'main' failed
make: *** [main] Error 1
Given the verbose and confusing error messages output by Clang you may think that the code is very far from being in a working state however you might be surprised to learn that this can be solved with a single keyword, mutable. Listing 2-24 shows the code in a proper compiling state.
Listing 2-24. Creating a mutable Closure
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = [myCopy](auto&& number) mutable {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
Listing 2-24 contains the solution to all of the error output that you can see above. The mutable keyword is used to tell the compiler that the lambda function should generate a closure with non-const members that have been copied by value.
The closures created by the compiler when they encounter a lambda function are const by default. This causes the compiler to create a type for the closure that can no longer be implicitly converted to a standard function pointer. The resulting error messages generated by a compiler when you try to use a lambda function to generate a closure that is not a suitable type for your code can be exceptionally confusing so there is no real solution here other than to properly learn how to use lambda functions and to compile often when working to pick up when you have made a change that the compiler cannot handle.
The next problem you might encounter while trying to compile the code that you’ve seen so far in this Recipe is to compile with a C++11 compiler that does not support C++14. The problem here is that C++11 lambdas do not support the auto keyword as a parameter. Building Listing 2-24 with a C++11 compiler results in the following output.
clang++ -g -std=c++11 main.cpp -o main
main.cpp:24:31: error: 'auto' not allowed in lambda parameter
auto myClosure = [myCopy](auto&& number) mutable {
^~~~
main.cpp:30:5: error: no matching function for call to 'PrintArray'
PrintArray(myClosure);
^~~~~~~~~~
main.cpp:12:6: note: candidate function not viable: no known conversion from '<lambda at main.cpp:24:22>' to 'const
std::function<void (MyArray::value_type)>' for 1st argument
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
^
main.cpp:35:5: error: 'auto' not allowed in lambda parameter
[](auto&& number){
^~~~
In file included from main.cpp:1:
In file included from /usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/algorithm:62:
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_algo.h:3755:2: error: no matching function for call to object
of type '<lambda at main.cpp:35:2>'
__f(*__first);
^~~
main.cpp:33:10: note: in instantiation of function template specialization
'std::for_each<__gnu_cxx::__normal_iterator<const unsigned int *, std::vector<unsigned int,
std::allocator<unsigned int> > >, <lambda at main.cpp:35:2> >' requested here
std::for_each(myCopy.cbegin(),
^
main.cpp:35:2: note: candidate template ignored: couldn't infer template argument '$auto-0-0'
[](auto&& number){
^
4 errors generated.
makefile:2: recipe for target 'main' failed
make: *** [main] Error 1
Thankfully this is a much clearer message than when trying to compile Listing 2-23 and it’s reasonably clear that C++11 does not support auto type deduction for lambda function parameters. Listing 2-25 shows the code needed to build a working program that copied an array into avector using a lambda function.
Listing 2-25. A C++11 Compatible Lambda Function
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = [&myCopy](const MyArray::value_type&number) {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](const MyVector::value_type&number){
std::cout << number << std::endl;
});
return 0;
}
The code in Listing 2-25 will work just fine with a C++11 compiler but it does result in lambda functions that are slightly less portable between different types. The lambda function used to print the values from myCopy can now only be used with the type defined byMyVector::value_type whereas the C++14 version could have been reused with any type that could be passed as input to cout.
It goes without saying that none of this code will compile with a C++98 compiler as C++98 does not support lambda functions.
Recipe 2-7. Working with Time
Problem
You would like to write portable programs that are aware of the current time or their execution time.
Solution
Modern C++ provides STL templates and classes that provide portable time handling capabilities.
How It Works
Getting the Current Date and Time
C++11 provides access to different real-time clocks in a given computer system. The implementation of each clock may be different depending on the computer system that you are running on itself however the general intent of each clock will remain the same. You can use thesystem_clock to query the current time from a system wide real time clock. This means that you can use this type of clock to get the current date and time for a computer whilst your program is running. Listing 2-26 shows how this can be achieved.
Listing 2-26. Getting the Current Date and Time
#include <ctime>
#include <chrono>
#include <iostream>
using namespace std;
using namespace chrono;
int main()
{
auto currentTimePoint = system_clock::now();
auto currentTime = system_clock::to_time_t( currentTimePoint );
auto timeText = ctime( ¤tTime );
cout << timeText << endl;
return 0;
}
The program in Listing 2-26 shows how to retrieve the current time from system_clock. You do this using the system_clock::now method. The object returned from now is a time_point that contains a representation of time offset from some epoch. The epoch is a reference time that the system uses to offset all other times. You will not have to worry about the epoch by using the same clock for all of your time work. However you will have to be aware that a time from one computer may not be transferrable directly to another if the systems use different epochs for their time.
The time_point structure cannot be printed out directly and there is no method to convert it to a string however the class does provide a method to convert the time_point object into a time_t object. The time_t type is an old C type that can be converted to a string representation using the ctime function. You can see the result of running this program in Figure 2-6.
Figure 2-6. The Current Time Printed to the Terminal
Comparing Times
You can also use the STL time capabilities to compare one time to another. Listing 2-27 shows how you can compare a time to another.
Listing 2-27. Comparing Times
#include <ctime>
#include <chrono>
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono;
using namespace literals;
int main()
{
auto startTimePoint = system_clock::now();
this_thread::sleep_for(5s);
auto endTimePoint = system_clock::now();
auto timeTaken = duration_cast<milliseconds>(endTimePoint - startTimePoint);
cout << "Time Taken: " << timeTaken.count() << endl;
return 0;
}
Listing 2-27 shows that you can call the now method on a clock multiple times and retrieve different values. The program gets a time into the startTimePoint variable then calls the sleep_for method on the current execution thread. This call causes the program to go to sleep for 5 seconds and calls the system_clock::now method again after it resumes. At this point you have two time_point objects that can be used to subtract one from the other. The duration_cast can then be used to turn the result of the subtraction into a concrete time with a given type of duration. The valid duration types are hours, minutes, seconds, milliseconds, microseconds and nanoseconds. The count method is then used on the duration object to get the actual number of milliseconds that elapsed between calls to now.
Note The code in Listing 2-27 uses a C++14 standard user-defined literal. The 5s passed to sleep for defines a literal of 5 seconds. There are also literals defined for h (hours), min (minutes), s (seconds), ms (milliseconds), us (microseconds) and ns (nanoseconds). These literals can all be applied to an integer literal to inform the compiler that you would like to create a literal of a duration object with the given type of time. Applying s to a character literal such as "A String"s tells the compiler to create a literal of type std::string. These literals are defined in thestd::literals namespace and are a C++14 only feature, meaning that they cannot be used in C++11 or C++98 code.
Figure 2-7 shows the output generated when this program is run.
Figure 2-7. Output from Several Runs of Listing 2-27
Figure 2-7 shows that the sleep_for method isn’t 100% accurate however it is reasonably close to 5000ms with each run. You can now see how you can use the now method to compare two time_points and it’s not much more of a stretch to imagine that you can create an if statement that only executes once a certain amount of time has passed.
Recipe 2-8. Understanding lvalue and rvalue References
Problem
C++ contains a distinction between an lvalue reference and an rvalue reference. You need to be able to understand these concepts to write optimal C++ programs.
Solution
Modern C++ contains two different reference operators, & (lvalue) and && (rvalue). These work hand-in-hand with move semantics to reduce the time spent copying objects in your programs.
How It Works
Move semantics are one of the headline features of the modern C++ programming language. Their usefulness is being significantly overplayed and programmers new to modern C++ programming may be tempted to jump head first into the shiny new feature and actually make their programs worse due to a lack of understanding as to when and why to use an rvalue reference over a lvalue reference.
To put it simply, an rvalue reference should be used to move construct or move assign objects in place of copy operations where appropriate. Move semantics should not be used to replace passing parameters to methods by const reference. A move operation could be faster than a copy, in the worst case it can be slower than a copy and it will always be slower than passing by const reference. This recipe will show you the difference between an lvalue reference, an rvalue reference, the copy and move class constructors and operators and show some performance issues related to each.
The code in Listing 2-28 shows the implementation for a simple class that uses a static counter value to keep track of the number of objects in memory at any given time.
Listing 2-28. A Class that Counts the Number of Instances
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
}
~MyClass()
{
--(*m_Member);
m_Member = nullptr;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
int main()
{
auto object1 = MyClass();
cout << object1.GetValue() << endl;
{
auto object2 = MyClass();
cout << object2.GetValue() << endl;
}
auto object3 = MyClass();
cout << object3.GetValue() << endl;
return 0;
}
The s_Counter static member in Listing 2-28 counts the number of active instances of the class that exist in memory at any given time. This is achieved by initializing the static to 0 and pre-incrementing the value in the MyClass constructor through the member integer pointer. The s_Counter value is also decremented in ~MyClass to ensure that the number never grows out of control. The need for an unconventional setup will become clear when you see the move constructor in action. The output generated by this program is shown in Figure 2-8.
Figure 2-8. The s_Counter variable in action
You can now extend MyClass to contain a copy constructor and determine the impact this has on the number of objects in memory at any given time. Listing 2-29 shows a program that includes a MyClass copy constructor.
Listing 2-29. Copying MyClass
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
cout << "Constructing: " << GetValue() << endl;
}
~MyClass()
{
--(*m_Member);
m_Member = nullptr;
cout << "Destructing: " << s_Counter << endl;
}
MyClass(const MyClass& rhs)
: m_Member{ rhs.m_Member }
{
++(*m_Member);
cout << "Copying: " << GetValue() << endl;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
MyClass CopyMyClass(MyClass parameter)
{
return parameter;
}
int main()
{
auto object1 = MyClass();
{
auto object2 = MyClass();
}
auto object3 = MyClass();
auto object4 = CopyMyClass(object3);
return 0;
}
The code in Listing 2-29 has added a copy constructor and a function to copy object3 into object4. This has the impact of needing two copies, one to copy object3 into parameter and one to copy parameter into object4. Figure 2-9 shows that the two copy operations have occurred and that there are also two subsequent destructors called to destroy these objects.
Figure 2-9. Copy Constructors in Action
Move constructors can be utilized to cut down on the complexity of a copy constructor. There will be just as many objects in flight however you can safely shallow copy an object in a move constructor thanks to the rvalue reference type that they are passed. A rvalue reference is a guarantee from the compiler that the object referenced by the variable was a temporary object. This means that you are free to cannibalize the object so that you can implement a copy operation faster than if the pre-existing state was needed to be preserved. Listing 2-30 shows how to add a move constructor to MyClass.
Listing 2-30. Adding a Move Constructor to MyClass
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
cout << "Constructing: " << GetValue() << endl;
}
~MyClass()
{
if (m_Member)
{
--(*m_Member);
m_Member = nullptr;
cout << "Destructing: " << s_Counter << endl;
}
else
{
cout << "Destroying a moved-from instance" << endl;
}
}
MyClass(const MyClass& rhs)
: m_Member{ rhs.m_Member }
{
++(*m_Member);
cout << "Copying: " << GetValue() << endl;
}
MyClass(MyClass&& rhs)
: m_Member{ rhs.m_Member }
{
cout << hex << showbase;
cout << "Moving: " << &rhs << " to " << this << endl;
cout << noshowbase << dec;
rhs.m_Member = nullptr;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
MyClass CopyMyClass(MyClass parameter)
{
return parameter;
}
int main()
{
auto object1 = MyClass();
{
auto object2 = MyClass();
}
auto object3 = MyClass();
auto object4 = CopyMyClass(object3);
return 0;
}
The code in Listing 2-30 adds a move constructor to MyClass. This has an immediate impact on the running code. You can see that the move constructor is being invoked in Figure 2-10.
Figure 2-10. Using a Move Constructor
The compiler has realized that the state of parameter in Listing 2-30 does not need to be maintained after the return statement has ended. This means that the code can invoke a move constructor to create object4. This creates a scenario for a possible optimization in your code. This example is trivial and therefore there may be minimal performance and memory benefits. If the class was more complicated then you would save the memory needed to have both objects in memory at the same time and the time taken to copy from one object to the other. The performance benefits of this can be seen in Listing 2-31.
Listing 2-31. Comparing Copy Constructors with Move Constructors
#include <chrono>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using namespace chrono;
using namespace literals;
class MyClass
{
private:
vector<string> m_String{
"This is a pretty long string that"
" must be copy constructed into"
" copyConstructed!"s
};
int m_Value{ 1 };
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
MyClass(MyClass&& rhs) = default;
int GetValue() const
{
return m_Value;
}
};
int main()
{
using MyVector = vector<MyClass>;
constexpr unsigned int ITERATIONS{ 1000000U };
MyVector copyConstructed(ITERATIONS);
int value{ 0 };
auto copyStartTime = high_resolution_clock::now();
for (unsigned int i=0; i < ITERATIONS; ++i)
{
MyClass myClass;
copyConstructed.push_back(myClass);
value = myClass.GetValue();
}
auto copyEndTime = high_resolution_clock::now();
MyVector moveConstructed(ITERATIONS);
auto moveStartTime = high_resolution_clock::now();
for (unsigned int i=0; i < ITERATIONS; ++i)
{
MyClass myClass;
moveConstructed.push_back(move(myClass));
value = myClass.GetValue();
}
auto moveEndTime = high_resolution_clock::now();
cout << value << endl;
auto copyDuration =
duration_cast<milliseconds>(copyEndTime - copyStartTime);
cout << "Copy lasted: " << copyDuration.count() << "ms" << endl;
auto moveDuration =
duration_cast<milliseconds>(moveEndTime - moveStartTime);
cout << "Move lasted: " << moveDuration.count() << "ms" << endl;
return 0;
}
The code in Listing 2-31 makes use of the default keyword to inform the compiler that we would like to use the default constructor, copy constructor and move constructor for this class. This is valid here because there is no manual memory management or behavior needed byMyClass. We simple want to construct, copy or move the members m_String and m_Value. The m_Value variable is used to try to prevent the compiler from over-optimizing our example and producing unexpected results. You can see that the move constructor is faster in this instance than the copy constructor in Figure 2-11.
Figure 2-11. Showing a Move Constructor can be Faster than a Copy Constructor
Recipe 2-9. Using Managed Pointers
Problem
You would like to automate the task of managing memory in your C++ programs.
Solution
Modern C++ provides the capability to automatically manage dynamically allocated memory.
How It Works
Using unique_ptr
C++ provides three smart pointer types that can be used to automatically manage the lifetime of dynamically allocated objects. Listing 2-32 shows the use of a unique_ptr.
Listing 2-32. Using unique_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
int m_Value{ 10 };
public:
MyClass()
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
int GetValue() const
{
return m_Value;
}
};
int main()
{
unique_ptr<MyClass> uniquePointer{ make_unique<MyClass>() };
cout << uniquePointer->GetValue() << endl;
return 0;
}
The code in Listing 3-32 manages to create and destroy a dynamically allocated object without ever using new or delete. The make_unique template handles calling new and the unique_ptr object handles calling delete when the unique_ptr instance goes out of scope. Unfortunately the make_unique template is a C++14 feature and does not exist in C++11. The code in Listing 2-33 shows how you can rectify this.
Listing 2-33. Creating Your Own make_unique
#include <iostream>
#include <memory>
using namespace std;
#if __cplusplus > 200400L && __cplusplus < 201200L
template <typename T, typename... Args>
unique_ptr<T> make_unique(Args... args)
{
return unique_ptr<T>{ new T(args...) };
}
#endif
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
int main()
{
unique_ptr<MyClass> uniquePointer{
make_unique<MyClass>("MyClass", 10) };
cout << uniquePointer->GetName() << endl;
cout << uniquePointer->GetValue() << endl;
return 0;
}
The code in Listing 2-33 uses another C++11 feature to create a make_unique template. The template is a variadic template and it can take as many arguments as you wish to pass to it. This is proven in the call to make unique where a string and an int are passed through to the MyClassconstructor. The __cplusplus preprocessor symbol is used to detect the version of C++ that the compiler is using to build. You may need to ensure that this is working properly with the compiler that you are using as not all compilers implement this correctly. This code will build in C++11 using the user supplied make_unique template and will compile in C++14 using the standard supplied make_unique template.
Unique pointers are exactly as you expect, they are unique and therefore your code cannot have more than a single instance of a unique_ptr pointing to the same object at the same time. It achieves this by preventing copy operations on unqiue_ptr instances. A unique_ptr can be moved however and this allows you to pass a unique_ptr around in your program. Listing 2-34 shows how you can use move semantics to pass a unqiue_ptr around your program.
Listing 2-34. Moving a unqiue_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MyUniquePtr = unique_ptr<MyClass>;
auto PassUniquePtr(MyUniquePtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
auto uniquePointer = make_unique<MyClass>("MyClass", 10);
auto newUniquePointer = PassUniquePtr(move(uniquePointer));
if (uniquePointer)
{
cout << "First Object Name: " << uniquePointer->GetName() << endl;
}
cout << "Second Object Name: " << newUniquePointer->GetName() << endl;
return 0;
}
The code in Listing 2-34 moves a unique_ptr instance into a function. That instance is then moved back out of the function into a second unique_ptr object. There’s no reason why the same unique_ptr couldn’t have been used in main other than to show that the original instance is not valid after it has been moved from. This is evident in the if call to check if the pointer is valid as this will fail when the code is executed. The unique_ptr can be used in this manner and the object pointed to by the instance will be deleted once it goes out of scope without having been moved from. The output from this program is shown in Figure 2-12.
Figure 2-12. Valid unique_ptr Instances Moved Through a Function
Using shared_ptr Instances
Where a unique_ptr can give you sole ownership over a single object that you can move around in a single pointer instance, a shared_ptr can give you shared ownership over a single object. This works by having a shared_ptr storing an internal reference count along with the pointer to the object and only deleting the object once all of the values have gone out of scope. Listing 2-35 shows the use of a shared_ptr.
Listing 2-35. Using a shared_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MySharedPtr = shared_ptr<MyClass>;
auto PassSharedPtr(MySharedPtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
auto sharedPointer = make_shared<MyClass>("MyClass", 10);
{
auto newSharedPointer = PassSharedPtr(sharedPointer);
if (sharedPointer)
{
cout << "First Object Name: " << sharedPointer->GetName() << endl;
}
cout << "Second Object Name: " << newSharedPointer->GetName() << endl;
}
return 0;
}
The shared_ptr in Listing 2-35 has a different to the unique_ptr that you have seen before. A shared_ptr can be copied through your program and you can have multiple pointers pointing to the same object. This can be seen in Figure 2-13 where the output from the First Object Name statement can be seen.
Figure 2-13. Using a shared_ptr
Using a weak_ptr
Modern C++ also allows you to hold weak references to smart pointers. This allows you to get a reference to a pointer to a shared object temporarily while you need it for as long as the shared object exists. Listing 2-36 shows how you can achieve this using a weak_ptr.
Listing 2-36. Using a weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MySharedPtr = shared_ptr<MyClass>;
using MyWeakPtr = weak_ptr<MyClass>;
auto PassSharedPtr(MySharedPtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
MyWeakPtr weakPtr;
{
auto sharedPointer = make_shared<MyClass>("MyClass", 10);
weakPtr = sharedPointer;
{
auto newSharedPointer = PassSharedPtr(sharedPointer);
if (sharedPointer)
{
cout << "First Object Name: " << sharedPointer->GetName() << endl;
}
cout << "Second Object Name: " << newSharedPointer->GetName() << endl;
auto sharedFromWeak1 = weakPtr.lock();
if (sharedFromWeak1)
{
cout << "Name From Weak1: " << sharedFromWeak1->GetName() << endl;
}
}
}
auto sharedFromWeak2 = weakPtr.lock();
if (!sharedFromWeak2)
{
cout << "Shared Pointer Out Of Scope!" << endl;
}
return 0;
}
You can see in Listing 2-36 that a weak_ptr can be assigned a shared_ptr however you cannot access the shared object directly through the weak pointer. Instead a weak pointer supplies a lock method. The lock method returns a shared_ptr instance pointing to the object that you are referencing. This shared_ptr holds the object alive for the entirety of its scope if it ends up being the last object pointing to the object. The lock method always returns a shared_ptr however the shared_ptr returned by lock will fail an if test if the object no longer exists. You can see this at the end of the main function where lock is called after the object has been deleted. Figure 2-14 shows that the weak_ptr cannot get a valid shared_ptr after this has occurred.
Figure 2-14. A weak_ptr Failing to lock a Deleted Object