© Ray Lischner 2020
R. LischnerExploring C++20https://doi.org/10.1007/978-1-4842-5961-0_21

21. Function Arguments

Ray Lischner1 
(1)
Ellicott City, MD, USA
 

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++ that is surprisingly complex and subtle.

Argument Passing

Read through Listing 21-1, then answer the questions that follow it.
import <algorithm>;
import <iostream>;
import <iterator>;
import <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);
}
Listing 21-1.

Function Arguments and Parameters

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.emplace_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.
import <algorithm>;
import <iostream>;
import <iterator>;
import <vector>;
void modify(int& x)
{
  x = 10;
}
int triple(int x)
{
  return 3 * x;
}
void print_vector(std::vector<int>& v)
{
  std::cout << "{ ";
  std::ranges::copy(v, 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);
}
Listing 21-2.

Pass Parameters by Reference

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::ranges::copy(v, 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 here:
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.emplace_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 print the same results with a range-based for loop, but I wanted to show the use of const_iterator.

String Arguments

Strings present a unique opportunity. It is common practice to pass const strings to functions, but there is an unfortunate mismatch between C++ and its ancestor, C, when it comes to strings.

C lacks a built-in string type. Nor does it have any string type in its standard library. A quoted string literal is equivalent to a char array to which the compiler appends a NUL character ('') to denote the end of the string. When a program constructs a C++ std::string from a quoted literal string, the std::string object must copy the contents of the string literal. What this means is that if a function declares a parameter with type std::string const& in order to avoid copying the argument, and the caller passes a string literal, that literal gets copied anyway.

A solution to this problem was added to C++ 17 in the std::string_view class. A string_view does not copy anything. Instead, it is a small, fast way to refer to a std::string or quoted string literal. So you can use std::string_view as a function parameter type as follows:
int prompted_read(std::string_view prompt)
{
  std::cout << prompt;
  int x{0};
  std::cin >> x;
  ignore_line();
  return x;
}

In Exploration 20, calling prompted_read("Value: ") required constructing a std::string object, copying the string literal into that object, and then passing the object to the function. But the compiler can build and pass a string_view without copying any data. A string_view object is a lightweight read-only view of an existing string. You can usually treat a string_view the same way you would a const std::string. Use string_view whenever you want to pass a read-only string to a function; the function argument can be a quoted string literal, another string_view, or a std::string object. The only caveat to using string_view is that the standard library still has not caught on to string_view and there are many parts of the library that accept only string and not string_view. In this book, when you see strings passed as std::string const& instead of std::string_view, it is because the function must call some standard library function that does not handle string_view arguments.

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
3.145.172.159