EXPLORATION 21

image

Function Arguments

This Exploration continues the examination of functions introduced in Exploration 20, by focusing on argument-passing. Take a closer look. Remember that arguments are the expressions that you pass to a function in a function call. Parameters are the variables that you declare in the function declaration. This Exploration introduces the topic of function arguments, an area of C++ 11 that is surprisingly complex and subtle.

Argument Passing

Read through Listing 21-1 then answer the questions that follow it.

Listing 21-1.  Function Arguments and Parameters

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
void modify(int x)
{
  x = 10;
}
 
int triple(int x)
{
  return 3 * x;
}
 
void print_vector(std::vector<int> v)
{
  std::cout << "{ ";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
  std::cout << "} ";
}
 
void add(std::vector<int> v, int a)
{
  for (auto iter(v.begin()), end(v.end()); iter != end; ++iter)
    *iter = *iter + a;
}
 
int main()
{
  int a{42};
  modify(a);
  std::cout << "a=" << a << ' ';
 
  int b{triple(14)};
  std::cout << "b=" << b << ' ';
 
  std::vector<int> data{ 10, 20, 30, 40 };
 
  print_vector(data);
  add(data, 42);
  print_vector(data);
}

Predict what the program will print.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Now compile and run the program. What does it actually print?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Were you correct? ________________ Explain why the program behaves as it does.

_____________________________________________________________

_____________________________________________________________

When I run the program, I get the following results:

a=42
b=42
{ 10 20 30 40 }
{ 10 20 30 40 }

Expanding on these results, you may have noticed the modify function does not actually modify the variable a in main(), and the add function does not modify data. Your compiler might even have issued warnings to that effect.

As you can see, C++ passes arguments by value—that is, it copies the argument value to the parameter. The function can do whatever it wants with the parameter, but when the function returns, the parameter goes away, and the caller never sees any changes the function made.

If you want to return a value to the caller, use a return statement, as was done in the triple function.

Rewrite the add function so it returns the modified vector to the caller.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Compare your solution with the following code block:

std::vector<int> add(std::vector<int> v, int a)
{
  std::vector<int> result{};
  for (auto i : v)
    result.push_back(i + a);
  return result;
}

To call the new add, you must assign the function’s result to a variable.

data = add(data, 42);

What is the problem with this new version of add?

_____________________________________________________________

_____________________________________________________________

Consider what would happen when you call add with a very large vector. The function makes an entirely new copy of its argument, consuming twice as much memory as it really ought to.

Pass-by-Reference

Instead of passing large objects (such as vectors) by value, C++ lets you pass them by reference. Add an ampersand (&) after the type name in the function parameter declaration. Change Listing 21-1 to pass vector parameters by reference. Also change the modify function, but leave the other int parameters alone. What do you predict will be the output?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Now compile and run the program. What does it actually print?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Were you correct? ________________ Explain why the program behaves as it does.

_____________________________________________________________

_____________________________________________________________

Listing 21-2 shows the new version of the program.

Listing 21-2.  Pass Parameters by Reference

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
void modify(int& x)
{
  x = 10;
}
 
int triple(int x)
{
  return 3 * x;
}
 
void print_vector(std::vector<int>& v)
{
  std::cout << "{ ";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
  std::cout << "} ";
}
 
void add(std::vector<int>& v, int a)
{
  for (auto iter(v.begin()), end(v.end()); iter != end; ++iter)
    *iter = *iter + a;
}
 
int main()
{
  int a{42};
  modify(a);
  std::cout << "a=" << a << ' ';
 
  int b{triple(14)};
  std::cout << "b=" << b << ' ';
 
  std::vector<int> data{ 10, 20, 30, 40 };
 
  print_vector(data);
  add(data, 42);
  print_vector(data);
}

When I run the program, I get the following results:

a=10
b=42
{ 10 20 30 40 }
{ 52 62 72 82 }

This time, the program modified the x parameter in modify and updated the vector’s contents in add.

Change the rest of the parameters to use pass-by-reference. What do you expect to happen?

_____________________________________________________________

_____________________________________________________________

Try it. What actually happens?

_____________________________________________________________

_____________________________________________________________

The compiler does not allow you to call triple(14) when triple’s parameter is a reference. Consider what would happen if triple attempted to modify its parameter. You can’t assign to a number, only to a variable. Variables and literals fall into different categories of expressions. In general terms, a variable is an lvalue, as are references. A literal is called an rvalue, and expressions that are built up from operators and function calls usually result in rvalues. When a parameter is a reference, the argument in the function call must be an lvalue. If the parameter is call-by-value, you can pass an rvalue.

Can you pass an lvalue to a call-by-value parameter? ________________

You’ve seen many examples that you can pass an lvalue. C++ automatically converts any lvalue to an rvalue when it needs to. Can you convert an rvalue to an lvalue? ________________

If you aren’t sure, try to think of the problem in more concrete terms: can you convert an integer literal to a variable? That means you cannot convert an rvalue to an lvalue. Except, sometimes you can, as the next section will explain.

const References

In the modified program, the print_vector function takes its parameter by reference, but it doesn’t modify the parameter. This opens a window for programming errors: you can accidentally write code to modify the vector. To prevent such errors, you can revert to call-by-value, but you would still have a memory problem if the argument is large. Ideally, you would be able to pass an argument by reference, but still prevent the function from modifying its parameter. Well, as it turns out, such a method does exist. Remember const? C++ lets you declare a function parameter const too.

void print_vector(std::vector<int> const& v)
{
  std::cout << "{ ";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
  std::cout << "} ";
}

Read the parameter declaration by starting at the parameter name and working your way from right to left. The parameter name is v; it is a reference; the reference is to a const object; and the object type is std::vector<int>. Sometimes, C++ can be hard to read, especially for a newcomer to the language, but with practice, you will soon read such declarations with ease.

CONST WARS

Many C++ programmers put the const keyword in front of the type, as demonstrated in the following:

void print_vector(const std::vector<int>& v)

For simple definitions, the const placement is not critical. For example, to define a named constant, you might use

const int max_width{80}; // maximum line width before wrapping

The difference between that and

int const max_width{80}; // maximum line width before wrapping

is small. But with a more complicated declaration, such as the parameter to print_vector, the different style is more significant. I find my technique much easier to read and understand. My rule of thumb is to keep the const keyword as close as possible to whatever it is modifying.

More and more C++ programmers are coming around to adopt the const-near-the-name style instead of const out in front. Again, this is an opportunity for you to be in the vanguard of the most up-to-date C++ programming trends. But you have to get used to reading code with const out in front, because you’re going to see a lot of it.

So, v is a reference to a const vector. Because the vector is const, the compiler prevents the print_vector function from modifying it (adding elements, erasing elements, changing elements, and so on). Go ahead and try it. See what happens if you throw in any one of the following lines:

v.push_back(10); // add an element
v.pop_back();    // remove the last element
v.front() = 42;  // modify an element

The compiler stops you from modifying a const parameter.

Standard practice is to use references to pass any large data structure, such as vector, map, or string. If the function has no intention of making changes, declare the reference as a const. For small objects, such as int, use pass-by-value.

If a parameter is a reference to const, you can pass an rvalue as an argument. This is the exception that lets you convert an rvalue to an lvalue. To see how this works, change triple’s parameter to be a reference to const.

int triple(int const& x)

Convince yourself that you can pass an rvalue (such as 14) to triple. Thus, the more precise rule is that you can convert an rvalue to a const lvalue, but not to a non-const lvalue.

const_iterator

One additional trick you have to know when using const parameters: if you need an iterator, use const_iterator instead of iterator. A const variable of type iterator is not very useful, because you cannot modify its value, so the iterator cannot advance. You can still modify the element by assigning to the dereferenced iterator (e.g., *iter). Instead, a const_iterator can be modified and advanced, but when you dereference the iterator, the resulting object is const. Thus, you can read values but not modify them. This means you can safely use a const_iterator to iterate over a const container.

void print_vector(std::vector<int> const& v)
{
  std::cout << "{ ";
  std::string separator{};
  for (std::vector<int>::const_iterator i{v.begin()}, end{v.end()}; i != end; ++i)
  {
    std::cout << separator << *i;
    separator = ", ";
 
  }
  std::cout << "} ";
}

You can do the same with a range-based for loop, but I wanted to show the use of const_iterator.

Multiple Output Parameters

You’ve already seen how to return a value from a function. And you’ve seen how a function can modify an argument by declaring the parameter as a reference. You can use reference parameters to “return” multiple values from a function. For example, you may want to write a function that reads a pair of numbers from the standard input, as shown in the following:

void read_numbers(int& x, int& y)
{
  std::cin >> x >> y;
}

Now that you know how to pass strings, vectors, and whatnot to a function, you can begin to make further improvements to the word-counting program, as you will see in the next Exploration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset