Answers for chapter 12
Exercise 12.1: How many elements do b1 and b2 have at the end of this code?
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}
There are 4 elements do b1
and b2
have at the end of the code, because they are sharing the elements.
Write your own version of the StrBlob class including the const versions of front and back.
Exercise 12.3: Does this class need const versions of push_back and pop_back? If so, add them. If not, why aren’t they needed?
No. A const function implies we cannot modify anything through it, whereas push_back & pop_back will modify the container's size. Furthermore, for this special case, a const version of pop_back() would yield interesting results:
inline void
strBlob::pop_back() const
{
check(0, "pop_back on empty strBlob.");
data->pop_back();
}
If we call the pop_back function on a const strBlob
object, because we didn't modify the shared_ptr data
, the compiler will not complain. However, the vector pop_back member removed the last element of the vector to which data
points. This actually breaks the const correctness of both the pop_back function and the strBlob
object we just defined.
This issue raises another question: what kind of member function should be declared as a const member function?
The answer is: a member function that inspects (rather than mutates) its object, since we never change the object through it. An object mutating function, such as pop_back() / push_back(), should never be called on a const object nor has it const version; applying a mutate function to an object we don't want to change is absolutly a logical mistake.
Refs:
Exercise 12.4: In our check function we didn’t check whether i was greater than zero. Why is it okay to omit that check?
i
is an unsigned size_type; it will never be less than 0
. So, we don't need to worry about the case where i
is below 0
.
Exercise 12.5: We did not make the constructor that takes an initializer_list explicit (§ 7.5.4, p. 296). Discuss the pros and cons of this design choice.
Pros:
explicit
will prevent implicit conversion during the object construction. Cons:
explicit
prevent the copy initialization from a list (using a =
)explicit
works only on constructors with a single argument.In general, there is no standard stating whether we should use explicit for initializer_list. However, if you follow the Google Guide, then this behavior is prohibited:
Constructors that take a single std::initializer_list parameter should also omit explicit, in order to support copy-initialization (e.g., MyType m = {1, 2};).
Refs:
Exercise 12.6: Write a function that returns a dynamically allocated vector of ints. Pass that vector to another function that reads the standard input to give values to the elements. Pass the vector to another function to print the values that were read. Remember to delete the vector at the appropriate time.
CODE: Q.1
Exercise 12.7: Redo the previous exercise, this time using shared_ptr.
CODE: Q.1
bool b() {
int* p = new int;
// ...
return p;
}
p
is converted to a bool. As an result, we are not able to free the memory to which p
points after the conversion, which will cause a memory leak problem. p
points. Whether or not we return p
as an int*
, any subsequent access to that through p
will be undefined.
Exercise 12.9: Explain what happens in the following code:
//The memory to which r points before will never be freed when q is assigned to r.
//Thus we have memory leak here.
int *q = new int(42), *r = new int(100);
r = q;
//When q2 is assigned to r2, the object to which r2 points is freed.
//The counter associated with int 42 increases by 1.
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;
Exercise 12.10: Explain whether the following call to the process function defined on page 464 is correct. If not, how would you correct the call?
shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));
The code is correct, because of two reasons:
p
is defined outside of the process functionp
Due to the first reason, p
has a default count of 1. The process function creates a temporary shared_ptr by coping p
, so the call keeps the total count of p
at 1, which means the object to which 'p' points is still valid after the process function is invoked.
Exercise 12.11: What would happen if we called process as follows?
process(shared_ptr<int>(p.get()));
It is incorrect. p.get()
returns a plain pointer to the object on which p
points. Consequently, we created another temporary shared_ptr that points to what p
is pointing to. After leaving the scope of the process function, the temporary shared_ptr destories itself and frees the object to which p
points. It leaves p
a dangling pointer.
Exercise 12.12: Using the declarations of p and sp explain each of the following calls to process. If the call is legal, explain what it does. If the callis illegal, explain why.
auto p = new int();
auto sp = make_shared<int>();
//ok, sp is passed as an copy to the process.
(a) process(sp);
//error, The process function requires a shared_ptr<int> type, and the constructor of shared_ptr does not support implicit conversion.
(b) process(new int());
//error, The process function requires a shared_ptr<int> type, and the constructor of shared_ptr does not support implicit conversion.
(c) process(p);
//Ok. However, such a usage should be avoided. As p is a plain pointer, assigning it to a shared_ptr implies that the shared_ptr will take over the management of the object that p points to. Afterwards, it is the user's responsibility to ensure that the object is available. Any behavior that might cause shared_ptr to free the object would result in p a dangling pointer.
(d) process(shared_ptr<int>(p));
Exercise 12.13: What happens if we execute the following code?
auto sp = make_shared<int>();
auto p = sp.get();
delete p;
Deleteing p
will make sp
a dangling pointer, since they refer to the same dynamic object. If sp
is a local shared_ptr, the issue will worsen as sp
will corrupt the contents of the memory to which it points.
Exercise 12.14: Write your own version of a function that uses a shared_ptr to manage a connection.
CODE: Q.1
Exercise 12.15: Rewrite the first exercise to use a lambda (§ 10.3.2, p. 388) in place of the end_connection function.
CODE: Q.1
Exercise 12.16: Compilers don’t always give easy-to-understand error messages if we attempt to copy or assign a unique_ptr. Write a program that contains these errors to see how your compiler diagnoses them.
##assign
#g++
ex_12_16.cc:20:7: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>& std::unique_ptr<_Tp, _Dp>::operator=(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’
p2 = p1;
#clang++
ex_12_16.cc:20:5: error: overload resolution selected deleted operator '='
p2 = p1;
##copy
#g++
ex_12_16.cc:19:28: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’
std::unique_ptr<int> p3(p1);
#clang++
ex_12_16.cc:19:23: error: call to deleted constructor of 'std::unique_ptr<int>'
std::unique_ptr<int> p3(p1);
CODE: Q.1
Exercise 12.17: Which of the following unique_ptr declarations are illegal or likely to result in subsequent program error? Explain what the problem is with each one.
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
//illegal, ix is not a pointer.
(a) IntP p0(ix);
//illegal, unique_ptr is designed for managing dynamic memory, however, pi is managed by the compiler since it resides in stack memory.
(b) IntP p1(pi);
//legal, however, mixing smart pointers and plain pointers is considered bad practice. When p2 is destoried, pi2 might become a dangling pointer.
(c) IntP p2(pi2);
//illegal, this is equivalent to b.
(d) IntP p3(&ix);
//legal
(e) IntP p4(new int(2048));
//legal, but bad practice. The result of the //get()// function should never be used to initialize smart pointers. The operation will result in two independent smart pointers pointing to the same dynamic object, which is undefined behavior. Furthermore, potential double memory free operations may corrupt new data that uses the memory.
(f) IntP p5(p2.get());
Exercise 12.18: Why doesn’t shared_ptr have a release member?
The release() member is designed for transferring ownership of a dynamic object between unique_ptrs. Shared_ptr shares the same object, so there is no need to transfer any “ownership” between them.
If you want to perform some similar operations to release() on shared_ptr, you may apply reset() member to the destination shared_ptr then assign the new source to it. As a result, the destination shared_ptr will point to the new source. This is how release() is supposed to work on unique_ptr.
Exercise 12.19: Define your own version of StrBlobPtr and update your StrBlob class with the appropriate friend declaration and begin and end members.
Exercise 12.20: Write a program that reads an input file a line at a time into a StrBlob and uses a StrBlobPtr to print each element in that StrBlob.
CODE: Q.1
Exercise 12.21: We could have written StrBlobPtr’s deref member as follows, Which version do you think is better and why?
std::string& deref() const
{
return (*check(curr, "dereference past end"))[curr];
}
Both versions work fine. I prefer the version in a book because it is more readable.
Exercise 12.22: What changes would need to be made to StrBlobPtr tocreate a class that can be used with a const StrBlob? Define a classnamed ConstStrBlobPtr that can point to a const StrBlob.
CODE: header test file
Notice: A member called isPtrEnd
is created as a comparison operation for the print loop, in place of the operation ==
between two StrBlobPtr objects. While technically it would be better to use operator overloading, I believe having the solution with limited knowledge will better help us to understand what we just learned.
Exercise 12.23: Write a program to concatenate two string literals, putting the result in a dynamically allocated array of char. Write a program to concatenate two library strings that have the same value as the literals used in the first program.
CODE: Q.1
Exercise 12.24: Write a program that reads a string from the standard input into a dynamically allocated character array. Describe how your program handles varying size inputs. Test your program by giving it a string of datathat is longer than the array size you’ve allocated.
To handle varying input sizes, we can either use the size information from inputs to allocate memory, or we can specify the maximum size that the memory can hold.
The first method requires a temporary storage space for the input. It can either be an ostringstream
or a std::string
. In the dynamic memory initialization, the size of inputs can be acquired from both by calling the size() member, then a copy operation can be performed from the temporary to the allocated array.
In the second method, the dynamic memory is given a fixed size. The size may be specified by the user. To transfer input to a dynamic array, we can either use the cin.get()
function to tranfser the data directly, or use strncpy
to copy.
Addtionally, we may use the unique_ptr
in place of new
and delete
to manage the dynamic memory.
CODE: Q.1
Exercise 12.25: Given the following new expression, how would you delete pa?
int *pa = new int[10];
In the code, a dynamic array is allocated with space for 10
integers. It can be freed by using the following statement:
delete [] pa;
Exercise 12.26: Rewrite the program on page 481 using an allocator.
CODE: Q.1
Exercise 12.27: The TextQuery and QueryResult classes use only capabilities that we have already covered. Without looking ahead, write your own versions of these classes.
Exercise 12.28: Write a program to implement text queries without defining classes to manage the data. Your program should take a file and interact with a user to query for words in that file. Use vector, map, and set containers to hold the data for the file and to generate the results for the queries.
Exercise 12.29: We could have written the loop to manage the interaction with the user as a do while (§ 5.4.4, p. 189) loop. Rewrite the loop to use a do while. Explain which version you prefer and why.
The do-while statement is my preferred method. It goes through our logic, which is inputing before condition checking is performed.
Exercise 12.30: Define your own versions of the TextQuery and QueryResult classes and execute the runQueries function from § 12.3.1 (p. 486).
Exercise 12.31: What difference(s) would it make if we used a vector instead of a set to hold the line numbers? Which approach is better? Why?
It is preferable to use the set approach since the vector method does not order the line numbers or eliminate duplicates.
Exercise 12.32: Rewrite the TextQuery and QueryResult classes to use a StrBlob instead of a vector<string> to hold the input file.
Exercise 12.33: In Chapter 15 we’ll extend our query system and will need some additional members in the QueryResult class. Add members named begin and end that return iterators into the set of line numbers returned by a given query, and a member named get_file that returns a shared_ptr to the file in the QueryResult object.