官术网_书友最值得收藏!

Working with const References or r-value References

A temporary object cannot be passed as an argument for a reference parameter. To accept temporary parameters, we need to use const references or r-value references. The r-value references are references that are identified by two ampersands, &&, and can only refer to temporary values. We will look at them in more detail in Lesson 4, Generic Programming and Templates.

We need to remember that a pointer is a value that represents the location of an object.

Being a value, it means that when we are accepting a parameter as a pointer, the pointer itself is passed as a value.

This means that the modification of the pointer inside the function is not going to be visible to the caller.

But if we are modifying the object the pointer points to, then the original object is going to be modified:

void modify_pointer(int* pointer) {

*pointer = 1;

pointer = 0;

}

int main() {

int a = 0;

int* ptr = &a;

modify_pointer(ptr);

std::cout << "Value: " << *ptr << std::endl;

std::cout << "Did the pointer change? " << std::boolalpha << (ptr == &a);

}

Most of the time, we can think of passing a pointer as passing a reference, with the caveat that you need to be aware that the pointer might be null.

Accepting a parameter as a pointer is mainly used for three reasons:

  • Traversing the elements of an array, by providing the start pointer and either the end pointer or the size of the array.
  • Optionally modifying a value. This means that the function modifies a value if it is provided.
  • Returning more than a single value. This is often done to set the value of a pointer passed as an argument and then return an error code to signal whether the operation was performed.

We will see in Lesson 4, Generic Programming and Templates, how features introduced in C++11 and C++17 allow us to avoid using pointers for some of these use cases, eliminating the possibility of some common classes of errors, such as dereferencing invalid pointers or accessing unallocated memory.

The options of passing by value or passing by reference are applicable to every single parameter the function expects, independently.

This means that a function can take some arguments by value and some by reference.

Returning Values from Functions

Up until now, we have seen how to provide values to a function. In this section, we will see how a function can provide value back to the caller.

We said earlier that the first part of a function declaration is the type returned by the function: this is often referred to as the function's return type.

All the previous examples used void to signal that they were returning nothing. Now, it is time to look at an example of a function returning a value:

int sum(int, int);

The previous function accepts two integers by value as parameters and returns an integer.

The invocation of the function in the caller code is an expression evaluating to an integer. This means that we can use it anywhere that an expression is allowed:

int a = sum(1, 2);

A function can return a value by using the return keyword, followed by the value it wants to return.

The function can use the return keyword several times inside its body, and each time the execution reaches the return keyword, the program will stop executing the function and go back to the caller, with the value returned by the function, if any. Let's look at the following code:

void rideRollercoasterWithChecks(int heightInCm) {

if (heightInCm < 100) {

std::cout << "Too short";

return;

}

if (heightInCm > 210) {

std::cout << "Too tall";

return;

}

rideRollercoaster();

// implicit return at the end of the function

}

A function also returns to the caller if it reaches the end of its body.

This is what we did in the earlier examples since we did not use the return keyword.

Not explicitly returning can be okay if a function has a void return type. However, it will give unexpected results if the function is expected to return a value: the returned type will have an unspecified value and the program will not be correct.

Be sure to enable the warning, as it will save you a lot of debugging time.

Note

It is surprising, but every major compiler allows the compiling of functions, which declare a return type other than void, but don't return a value.

This is easy to spot in simple functions, but it is much harder in complex ones with lots of branches.

Every compiler supports options to warn you if a function returns without providing a value.

Let's look at an example of a function returning an integer:

int sum(int a, int b) {

return a + b;

}

As we said earlier, a function can use the return statement several times inside its body, as shown in the following example:

int max(int a, int b) {

if(a > b) {

return a;

} else {

return b;

}

}

We always return a value that's independent of the values of the arguments.

Note

It is a good practice to return as early as possible in an algorithm.

The reason for this is that as you follow the logic of the code, especially when there are many conditionals, a return statement tells you when that execution path is finished, allowing you to ignore what happens in the remaining part of the function.

If you only return at the end of the function, you always have to look at the full code of the function.

Since a function can be declared to return any type, we have to decide whether to return a value or a reference.

Returning by Value

A function whose return type is a value type is said to return by value.

When a function that returns by value reaches a return statement, the program creates a new object, which is initialized from the value of the expression in the return statement.

In the previous function, sum, when the code reaches the stage of returning a + b, a new integer is created, with the value equal to the sum of a and b, and is returned.

On the side of the caller, int a = sum(1,2);, a new temporary automatic object is created and is initialized from the value returned by the function (the integer that was created from the sum of a and b).

This object is called temporary because its lifetime is valid only while the full-expression in which it is created is executed. We will see in the Returning by Reference section, what this means and why this is important.

The calling code can then use the returned temporary value in another expression or assign it to a value.

Add the end of the full expression, since the lifetime of the temporary object is over, it is destroyed.

In this explanation, we mentioned that objects are initialized several times while returning a value. This is not a performance concern as C++ allows compilers to optimize all these initializations, and often initialization happens only once.

Note

It is preferable to return by value as it's often easier to understand, easier to use, and as fast as returning by reference.

How can returning by value be so fast? C++11 introduced the move semantic, which allows moving instead of copying the return types when they support the move operation. We'll see how in Lesson 3, Classes. Even before C++11, all mainstream compilers implemented return value optimization (RVO) and named return value optimization (NRVO), where the return value of a function is constructed directly in the variable into which they would have been copied to when returned. In C++17, this optimization, also called copy elision, became mandatory.

Returning by Reference

A function whose return type is a reference is said to return by reference.

When a function returning a reference reaches a return statement, a new reference is initialized from the expression that's used in the return statement.

In the caller, the function call expression is substituted by the returned reference.

However, in this situation, we need to also be aware of the lifetime of the object the reference is referring to. Let's look at an example:

const int& max(const int& a, const int& b) {

if (a > b) {

return a;

} else {

return b;

}

}

First, we need to note that this function already has a caveat. The max function is returning by value, and it did not make a difference if we returned a or b when they were equal.

In this function, instead, when a == b we are returning b, this means that the code calling this function needs to be aware of this distinction. In the case where a function returns a non-const reference it might modify the object referred to by the returned reference, and whether a or b is returned might make a difference.

We are already seeing how references can make our code harder to understand.

Let's look at the function we used:

int main() {

const int& a = max(1,2);

std::cout << a;

}

This program has an error! The reason is that 1 and 2 are temporary values, and as we explained before, a temporary value is alive until the end of the full expression containing it.

To better understand what is meant by "the end of the full expression containing it", let's look at the code we have in the preceding code block: int& a = max(1,2);. There are four expressions in this piece of code:

  • 1 is an integer literal, which still counts as an expression
  • 2 is an integer literal, similar to 1
  • max(expression1, expression2) is a function call expression
  • a = expression3 is an assignment expression

All of this happens in the declaration statement of variable a.

The third point covers the function call expression, while containing the full expression is covered in the following point.

This means that lifetimes 1 and 2 will stop at the end of the assignment. But we got a reference to one of them! And we are using it!

Accessing an object whose lifetime is terminated is prohibited by C++, and this will result in an invalid program.

In a more complex example, such as int a = max(1,2) + max(3,4);, the temporary objects returned by the max functions will be valid until the end of the assignment, but no longer.

Here, we are using the two references to sum them, and then we assign the result as a value. If we assigned the result to a reference, as in the following example, int& a = max(1,2) + max(3,4);, instead, the program would have been wrong.

This sounds confusing, but it is important to understand as it can be a source of hard-to-debug problems if we use a temporary object after the full expression in which it's created has finished executing.

Let's look at another common mistake in functions returning references:

int& sum(int a, int b) {

int c = a + b;

return c;

}

We created a local, automatic object in the function body and then we returned a reference to it.

In the previous section, we saw that local objects' lifetimes end at the end of the function. This means that we are returning a reference to an object whose lifetime will always be terminated.

Earlier, we mentioned the similarities between passing arguments by reference and passing arguments by pointers.

This similarity persists when returning pointers: the object pointed to by a pointer needs to be alive when the pointer is later dereferenced.

So far, we have covered examples of mistakes while returning by reference. How can references be used correctly as return types to functions?

The important part of using references correctly as return values is to make sure that the object outlives the reference: the object must always be alive – at least until there is a reference to it.

A common example is accessing a part of an object, for example, using an std::array, which is a safe option compared to the built-in array:

int& getMaxIndex(std::array<int, 3>& array, int index1, int index2) {

/* This function requires that index1 and index2 must be smaller than 3! */

int maxIndex = max(index1, index2);

return array[maxIndex];

The calling code would look as follows:

int main() {

std:array<int, 3> array = {1,2,3};

int& elem = getMaxIndex(array, 0, 2);

elem = 0;

std::cout << array[2];

// Prints 0

}

In this example, we are returning a reference to an element inside an array, and the array remains alive longer than the reference.

The following are guidelines for using return by reference correctly:

  • Never return a reference to a local variable (or a part of it)
  • Never return a reference to a parameter accepted by value (or a part of it)

When returning a reference that was received as a parameter, the argument passed to the function must live longer than the returned reference.

Apply the previous rule, even when you are returning a reference to a part of the object (for example, an element of an array).

Activity 4: Using Pass by Reference and Pass by Value

In this activity, we are going to see the different trade-offs that can be made when writing a function, depending on the parameters the function accepts:

  1. Write a function that takes two numbers and returns the sum. Should it take the arguments by value or reference? Should it return by value or by reference?
  2. After that, write a function that takes two std::arrays of ten integers and an index (guaranteed to be less than 10) and returns the greater of the two elements to the given index in the two arrays.
  3. The calling function should then modify the element. Should it take the arguments by value or reference? Should it return by value or by reference? What happens if the values are the same?

Take the arrays by reference and return by reference because we are saying that the calling function is supposed to modify the element. Take the index by value since there is no reason to use references.

If the values are the same, the element from the first array is returned.

Note

The solution to this activity can be found at page 285.

主站蜘蛛池模板: 寻乌县| 丁青县| 亳州市| 哈尔滨市| 会同县| 定日县| 成安县| 长岛县| 靖西县| 武定县| 晴隆县| 工布江达县| 云和县| 和龙市| 莱州市| 进贤县| 突泉县| 报价| 深水埗区| 深水埗区| 龙州县| 泗洪县| 隆德县| 乡城县| 高青县| 芷江| 梧州市| 成安县| 汝南县| 右玉县| 平南县| 枣阳市| 东乡| 临清市| 临清市| 务川| 常州市| 马龙县| 山阴县| 公安县| 拉萨市|