EXPLORATION 24

image

Overloading Function Names

In C++, multiple functions can have the same name, provided the functions have a different number of arguments or different argument types. Using the same name for multiple functions is called overloading and is common in C++.

Overloading

All programming languages use overloading at one level or another. For example, most languages use + for integer addition as well as for floating-point addition. Some languages, such as Pascal, use different operators for integer division (div) and floating-point division (/), but others, such as C and Java, use the same operator (/).

C++ takes overloading one step further, letting you overload your own function names. Judicious use of overloading can greatly reduce complexity in a program and make your programs easier to read and understand.

For example, C++ inherits several functions from the standard C library that compute an absolute value: abs takes an int argument; fabs takes a floating-point argument; and labs takes a long integer argument.

image Note  Don’t be concerned that I have not yet covered these other types. All that matters for the purpose of this discussion is that they are distinct from int. The next Exploration will begin to examine them more closely, so please be patient.

C++ also has its own complex type for complex numbers, which has its own absolute value function. In C++, however, they all have the same name, std::abs. Using different names for different types merely clutters the mental landscape and contributes nothing to the clarity of the code.

The sort function, just to name one example, has two overloaded forms:

std::sort(start, end);
std::sort(start, end, compare);

The first form sorts in ascending order, comparing elements with the < operator, and the second form compares elements by calling compare. Overloading appears in many other places in the standard library. For example, when you create a locale object, you can copy the global locale by passing no arguments

std::isalpha('X', std::locale{});

or create a native locale object by passing an empty string argument

std::isalpha('X', std::locale{""});

Overloading functions is easy, so why not jump in? Write a set of functions, all named print. They all have a void return type and take various parameters.

  • One takes an int as a parameter. It prints the parameter to the standard output.
  • Another takes two int parameters. It prints the first parameter to the standard output and uses the second parameter as the field width.
  • Another takes a vector<int> as the first parameter, followed by three string parameters. Print the first string parameter, then each element of the vector (by calling print), with the second string parameter between elements, and the third string parameter after the vector. If the vector is empty, print the first and third string parameters only.
  • Another has the same parameters as the vector form, but also takes an int as the field width for each vector element.

Write a program to print vectors using the print functions. Compare your functions and program with mine in Listing 24-1.

Listing 24-1.  Printing Vectors by Using Overloaded Functions

#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
 
void print(int i)
{
  std::cout << i;
}
 
void print(int i, int width)
{
  std::cout.width(width);
  std::cout << i;
}
 
void print(std::vector<int> const& vec,
    int width,
    std::string const& prefix,
    std::string const& separator,
    std::string const& postfix)
{
  std::cout << prefix;
 
  bool print_separator{false};
  for (auto x : vec)
  {
    if (print_separator)
 
      std::cout << separator;
    else
      print_separator = true;
    print(x, width);
  }
 
  std::cout << postfix;
}
 
void print(std::vector<int> const& vec,
    std::string const& prefix,
    std::string const& separator,
    std::string const& postfix)
{
  print(vec, 0, prefix, separator, postfix);
}
 
int main()
{
  std::vector<int> data{ 10, 20, 30, 40, 100, 1000, };
 
  std::cout << "columnar data: ";
  print(data, 10, "", " ", " ");
  std::cout << "row data: ";
  print(data, "{", ", ", "} ");
}

The C++ library often uses overloading. For example, you can change the size of a vector by calling its resize member function. You can pass one or two arguments: the first argument is the new size of the vector. If you pass a second argument, it is a value to use for new elements, in case the new size is larger than the old size.

data.resize(10);      // if the old size < 10, use default of 0 for new elements
data.resize(20, -42); // if the old size < 20, use -42 for new elements

Library-writers often employ overloading, but applications programmers use it less often. Practice writing libraries by writing the following functions:

bool is_alpha(char ch)

Returns true if ch is an alphabetic character in the global locale; if not, returns false.

bool is_alpha(std::string const& str)

Returns true, if str contains only alphabetic characters in the global locale, or false, if any character is not alphabetic. Returns true if str is empty.

char to_lower(char ch)

Returns ch after converting it to lowercase, if possible; otherwise, returns ch. Use the global locale.

std::string to_lower(std::string str)

Returns a copy of str after converting its contents to lowercase, one character at a time. Copies verbatim any character that cannot be converted to lowercase.

char to_upper(char ch)

Returns ch after converting it to uppercase, if possible; otherwise, returns ch. Use the global locale.

std::string to_upper(std::string str)

Returns a copy of str after converting its contents to uppercase, one character at a time. Copies verbatim any character that cannot be converted to uppercase.

Compare your solution with mine, which is shown in Listing 24-2.

Listing 24-2.  Overloading Functions in the Manner of a Library-Writer

#include <algorithm>
#include <iostream>
#include <locale>
 
bool is_alpha(char ch)
{
  return std::isalpha(ch, std::locale{});
}
 
bool is_alpha(std::string const& str)
{
  for (char ch : str)
    if (not is_alpha(ch))
      return false;
  return true;
}
 
char to_lower(char ch)
{
  return std::tolower(ch, std::locale{});
}
 
std::string to_lower(std::string str)
{
  for (char& ch : str)
    ch = to_lower(ch);
  return str;
}
  
char to_upper(char ch)
{
  return std::toupper(ch, std::locale{});
}
 
std::string to_upper(std::string str)
{
  for (char& ch : str)
    ch = to_upper(ch);
  return str;
}
 
int main()
{
  std::string str{};
  while (std::cin >> str)
  {
    if (is_alpha(str))
      std::cout << "alpha ";
    else
      std::cout << "not alpha ";
    std::cout << "lower: " << to_lower(str) << " upper: " << to_upper(str) << ' ';
  }
}

After waxing poetic about the usefulness of standard algorithms, such as transform, I turned around and wrote my own loops. If you tried to use a standard algorithm, I applaud you for your effort and apologize for tricking you.

If you want to pass an overloaded function to a standard algorithm, the compiler has to be able to tell which overloaded function you really mean. For some rather complicated reasons, the compiler has difficulty understanding situations such as the following:

std::string to_lower(std::string str)
{
  std::transform(str.begin(), str.end(), str.begin(), to_lower);
 
  return str;
}

Because to_lower is overloaded, the compiler does not know which to_lower you mean. C++ has ways to help the compiler understand what you mean, but it involves some nasty-looking code, and I’d rather you stay away from it for the time being. If you really insist on punishing yourself, look at code that works, but don’t try to sprain your brain understanding it.

std::string to_lower(std::string str)
{
  std::transform(str.begin(), str.end(), str.begin(),
                 static_cast<char (*)(char)>(to_lower));
  return str;
}

If you look closely at to_upper and to_lower, you’ll notice a couple of techniques that are different from other, similar functions. Can you spot them? If so, what are they?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The range-based for loops work with references! Like a pass-by-reference function parameter, the declaration

char& ch

is a reference to a character in the range. Thus, assigning to ch changes the character in the string. Note that auto can also declare a reference. If each element of the range is large, you should use references to avoid making unnecessary copies. Use a const reference if you do not have to modify the element, as follows:

for (auto const& big_item : container_full_of_big_things)

The other anomaly is that to_lower and to_upper string functions do not take const references but take plain strings as parameters. This means the argument is passed by value, which in turn means the compiler arranges to copy the string when passing the argument to the function. The function requires the copy, so this technique helps the compiler generate optimal code for copying the argument, and it saves you a step in writing the function. It’s a small trick, but a useful one. This technique will be especially useful later in the book—so don’t forget it.

The is_alpha string function does not modify its parameter, so it can take a const reference.

A common use of overloading is to overload a function for different types, including different integer types, such as a long integer. The next Exploration takes a look at these other types.

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

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