EXPLORATION 40

image

Declarations and Definitions

Exploration 20 introduced the distinction between declarations and definitions. This is a good time to remind you of the difference and to explore declarations and definitions of classes and their members.

Declaration vs. Definition

Recall that a declaration furnishes the compiler with the basic information it needs, so that you can use a name in a program. In particular, a function declaration tells the compiler about the function’s name, return type, parameter types, and modifiers, such as const and override.

A definition is a particular kind of declaration that also provides the full implementation details for an entity. For example, a function definition includes all the information of a function declaration, plus the function body. Classes, however, add another layer of complexity, because you can declare or define the class’s members independently of the class definition itself. A class definition must declare all of its members. Sometimes, you can also define a member function as part of a class definition (which is the style I’ve been using so far), but most programmers prefer to declare member functions inside the class and to define the member functions separately, outside of the class definition.

As with any function declaration, a member function declaration includes the return type (possibly with a virtual specifier), the function name, the function parameters, and an optional const or override modifier. If the function is a pure virtual function, you must include the = 0 token marks as part of the function declaration, and you don’t define the function.

The function definition is like any other function definition, with a few exceptions. The definition must follow the declaration—that is, the member function definition must come later in the source file than the class definition that declares the member function. In the definition, omit the virtual and override specifiers. The function name must start with the class name, followed by the scope operator (::) and the function name, so that the compiler knows which member function you are defining. Write the function body the same way you would write it if you provided the function definition inside the class definition. Listing 40-1 shows some examples.

Listing 40-1.  Declarations and Definitions of Member Functions

class rational
{
public:
  rational();
  rational(int num);
  rational(int num, int den);
  void assign(int num, int den);
  int numerator() const;
  int denominator() const;
  rational& operator=(int num);
private:
  void reduce();
  int numerator_;
  int denominator_;
};
 
rational::rational()
: rational{0}
{}
 
rational::rational(int num)
: numerator_{num}, denominator_{1}
{}
 
rational::rational(int num, int den)
: numerator_{num}, denominator_{den}
{
  reduce();
}
 
void rational::assign(int num, int den)
{
  numerator_ = num;
  denominator_ = den;
  reduce();
}
 
void rational::reduce()
{
  assert(denominator_ != 0);
  if (denominator_ < 0)
  {
    denominator_ = -denominator_;
    numerator_ = -numerator_;
  }
  int div{gcd(numerator_, denominator_)};
  numerator_ = numerator_ / div;
  denominator_ = denominator_ / div;
}
 
int rational::numerator()
const
{
  return numerator_;
}
 
int rational::denominator()
const
{
  return denominator_;
}
 
rational& rational::operator=(int num)
{
  numerator_ = num;
  denominator_ = 1;
  return *this;
}

Because each function name begins with the class name, the full constructor name is rational :: rational, and member function names have the form rational :: numerator, rational :: operator=, etc. The C++ term for the complete name is qualified name.

Programmers have many reasons to define member functions outside the class. The next section presents one way that functions differ depending on where they are defined, and the next Exploration will focus on this thread in detail.

inline Functions

In Exploration 30, I introduced the inline keyword, which is a hint to the compiler that it should optimize speed over size by trying to expand a function at its point of call. You can use inline with member functions too. Indeed, for trivial functions, such as those that return a data member and do nothing else, making the function inline can improve speed and program size.

When you define a function inside the class definition, the compiler automatically adds the inline keyword. If you separate the definition from the declaration, you can still make the function inline by adding the inline keyword to the function declaration or definition. Common practice is to place the inline keyword only on the definition, but I recommend putting the keyword in both places, to help the human reader.

Remember that inline is just a hint. The compiler does not have to heed the hint. Modern compilers are becoming better and better at making these decisions for themselves.

My personal guideline is to define one-line functions in the class definition. Longer functions or functions that are complicated to read belong outside the class definition. Some functions are too long to fit in the class definition but are short and simple enough that they should be inline. Organizational coding styles usually include guidelines for inline functions. For example, directives for large projects may eschew inline functions because they increase coupling between software components. Thus, inline may be allowed only on a function-by-function basis, when performance measurements demonstrate their need.

Rewrite the rational class from Listing 40-1 to use inline functions judiciously. Compare your solution with that of mine, shown in Listing 40-2.

Listing 40-2.  The rational Class with inline Member Functions

class rational
{
public:
  rational(int num) : numerator_{num}, denominator_{1} {}
  rational(rational const&) = default;
  inline rational(int num, int den);
  void assign(int num, int den);
  int numerator() const                   { return numerator_; }
  int denominator() const                 { return denominator_; }
  rational& operator=(int num);
private:
  void reduce();
  int numerator_;
  int denominator_;
};
 
inline rational::rational(int num, int den)
: numerator_{num}, denominator_{den}
{
  reduce();
}
 
void rational::assign(int num, int den)
{
  numerator_ = num;
  denominator_ = den;
  reduce();
}
 
void rational::reduce()
{
  assert(denominator_ != 0);
  if (denominator_ < 0)
  {
    denominator_ = -denominator_;
    numerator_ = -numerator_;
  }
  int div{gcd(numerator_, denominator_)};
  numerator_ = numerator_ / div;
  denominator_ = denominator_ / div;
}
 
rational& rational::operator=(int num)
{
  numerator_ = num;
  denominator_ = 1;
  return *this;
}

Don’t agonize over deciding which functions should be inline. When in doubt, don’t bother. Make functions inline only if performance measures show that the function is called often and the function call overhead is significant. In all other aspects, I regard the matter as one of aesthetics and clarity: I find one-line functions are easier to read when they are inside the class definition.

Variable Declarations and Definitions

Ordinary data members have declarations, not definitions. Local variables in functions and blocks have definitions, but not separate declarations. This can be a little confusing, but don’t be concerned, I’ll unravel it and make it clear.

A definition of a named object instructs the compiler to set aside memory for storing the object’s value and to generate the necessary code to initialize the object. Some objects are actually sub-objects—not entire objects on their own (entire objects are called complete objects in C++ parlance). A sub-object doesn’t get its own definition; instead, its memory and lifetime are dictated by the complete object that contains it. That’s why a data member or base class doesn’t get a definition of its own. Instead, the definition of an object with class type causes memory to be set aside for all of the object’s data members. Thus, a class definition contains declarations of data members but not definitions.

You define a variable that is local to a block. The definition specifies the object’s type, name, whether it is const, and the initial value (if any). You can’t declare a local variable without defining it, but there are other kinds of declarations.

You can declare a local reference as a synonym for a local variable. Declare the new name as a reference in the same manner as a reference parameter, but initialize it with an existing object. If the reference is const, you can use any expression (of a suitable type) as the initializer. For a non-const reference, you must use an lvalue (remember those from Exploration 21?), such as another variable. Listing 40-3 illustrates these principles.

Listing 40-3.  Declaring and Using References

#include <iostream>
 
int main()
{
  int answer{42};    // definition of a named object, also an lvalue
  int& ref{answer};  // declaration of a reference named ref
  ref = 10;          // changes the value of answer
  std::cout << answer << ' ';
  int const& cent{ref * 10}; // declaration; must be const to initialize with expr
  std::cout << cent << ' ';
}

A local reference is not a definition, because no memory is allocated, and no initializers are run. Instead, the reference declaration creates a new name for an old object. One common use for a local reference is to create a short name for an object that is obtained from a longer expression, and another is to save a const reference to an expression, so that you can use the result multiple times. Listing 40-4 shows a silly program that reads a series of integers into a vector, sorts the data, and searches for all the elements that equal a magic value. It does this by calling the equal_range algorithm, which returns a pair (first described in Exploration 15) of iterators that delimit a range of equal values.

Listing 40-4.  Finding 42 in a Data Set

#include <algorithm>
#include <iostream>
#include <iterator>
#include <utility>
#include <vector>
 
int main()
{
  std::vector<int> data{std::istream_iterator<int>(std::cin), std::istream_iterator<int>()};
  std::sort(data.begin(), data.end());
  // Find all values equal to 42
  auto const& range( std::equal_range(data.begin(), data.end(), 42) );
  if (range.first != range.second)
  {
    // Print the range indices only if at least one value is found.
    std::cout << "index of start of range: " << range.first  - data.begin() << ' ';
    std::cout << "index of end of range:   " << range.second - data.begin() << ' ';
  }
  std::cout << "size of range:           " << range.second - range.first << ' ';
}

If you define range as a local variable instead of declaring it as a reference, the program would work just fine, but it would also make an unneeded copy of the result that equal_range returns. In this program, the extra copy is irrelevant and unnoticeable, but in other programs, the cost savings can add up.

What happens if you delete theconst from the declaration ofrange ?

_____________________________________________________________

_____________________________________________________________

The result that equal_range returns is an rvalue, not an lvalue, so you must use const when initializing a reference to that result. Because you are free to modify an object via a non-const reference, only lvalue objects are allowed. Usually, the values returned from functions are rvalues, not lvalues, so references must be const.

Static Variables

Local variables are automatic. This means that when the function begins or a local block (compound statement) is entered, memory is allocated, and the object is constructed. When the function returns or when control exits the block, the object is destroyed, and memory is reclaimed. All automatic variables are allocated on the program stack, so memory allocation and release is trivial and typically handled by the host platform’s normal function-call instructions.

Remember that main() is like a function and follows many of the same rules as other functions. Thus, variables that you define in main() seem to last for the entire lifetime of the program, but they are automatic variables, allocated on the stack, and the compiler treats them the same as it treats any other automatic variables.

The behavior of automatic variables permits idioms such as RAII (see Exploration 39) and greatly simplifies typical programming tasks. Nonetheless, it is not suited for every programming task. Sometimes you need a variable’s lifetime to persist across function calls. For example, suppose you need a function that generates unique identification numbers for a variety of objects. It starts a serial counter at 1 and increments the counter each time it issues an ID. Somehow, the function must keep track of the counter value, even after it returns. Listing 40-5 demonstrates one way to do it.

Listing 40-5.  Generating Unique Identification Numbers

int generate_id()
{
  static int counter{0};
  ++counter;
  return counter;
}

The static keyword informs the compiler that the variable is not automatic but static. The first time the program calls generate_id(), the variable counter is initialized. The memory is not automatic and is not allocated on the program stack. Instead, all static variables are kept off to the side somewhere, so they don’t go away until the program shuts down. When generate_id() returns, counter is not destroyed and, therefore, retains its value.

Write a program to callgenerate_id() multiple times, to see that it works and generates new values each time you call it. Compare your program with mine, which is shown in Listing 40-6.

Listing 40-6.  Calling generate_id to Demonstrate Static Variables

#include <iostream>
 
int generate_id()
{
  static int counter{0};
  ++counter;
  return counter;
}
 
int main()
{
  for (int i{0}; i != 10; ++i)
    std::cout << generate_id() << ' ';
}

You can also declare a variable outside of any function. Because it is outside of all functions, it is not inside any block, thus it cannot be automatic, and so its memory must be static. You don’t have to use the static keyword for such a variable. Rewrite Listing 40-6 to declarecounter outside of thegenerate_id function. Do not use the static keyword. Assure yourself that the program still works correctly. Listing 40-7 shows my solution.

Listing 40-7.  Declaring counter Outside of the generate_id Function

#include <iostream>
 
int counter;
 
int generate_id()
{
  ++counter;
  return counter;
}
 
int main()
{
  for (int i{0}; i != 10; ++i)
    std::cout << generate_id() << ' ';
}

Unlike automatic variables, all static variables without initializers start out filled with zero, even if the variable has a built-in type. If the class has a custom constructor, the default constructor is then called to initialize static variables of class type. Thus, you don’t have to specify an initializer for counter, but you can if you want to.

One of the difficulties in working with static variables in C++ is that you have little control over when static variables are initialized. The standard offers two basic guarantees:

  • Static objects are initialized in the same order as their order of appearance in the file.
  • Static objects are initialized before their first use in main(), or any function called from main().

Prior to the start of main(), however, you have no guarantee that a static object will be initialized when you expect it to be. In practical terms, this means a constructor for a static object should not refer to other static objects, because those other objects may not be initialized yet. All names in C++ are lexically scoped; a name is visible only within its scope. The scope for a name declared within a function is the block that contains the declaration (including the statement header of for, if, and while statements). The scope for a name declared outside of any function is a little trickier. The name of a variable or function is global and can be used only for that single entity throughout the program. On the other hand, you can use it only in the source file where it is declared, from the point of declaration to the end of the file. (The next Exploration will go into more detail about working with multiple source files.)

The common term for variables that you declare outside of all functions is global variables. That’s not the standard C++ terminology, but it will do for now.

If you declare counter globally, you can refer to it and modify it anywhere else in the program, which may not be what you want. It’s always best to limit the scope of every name as narrowly as possible. By declaring counter inside generate_id, you guarantee that no other part of the program can accidentally change its value. In other words, if only one function has to access a static variable, keep the variable’s definition local to the function. If multiple functions must share the variable, define the variable globally.

Static Data Members

The static keyword has many uses. You can use it before a member declaration in a class to declare a static data member. A static data member is one that is not part of any objects of the class but, instead, is separate from all objects. All objects of that class type (and derived types) share a sole instance of the data member. A common use for static data members is to define useful constants. For example: The std :: string class has a static data member, npos, which roughly means “no position.” Member functions return npos when they cannot return a meaningful position, such as find when it cannot find the string for which it was looking. You can also use static data members to store shared data the same way a globally static variable can be shared. By making the shared variable a data member, however, you can restrict access to the data member using the normal class access levels.

Define a static data member the way you would any other global variable but qualify the member name with the class name. Use the static keyword only in the data member’s declaration, not in its definition. Because static data members are not part of objects, do not list them in a constructor’s initializer list. Instead, initialize static data members the way you would an ordinary global variable, but remember to qualify the member name with the class name. Qualify the name when you use a static data member too. Listing 40-8 shows some simple uses of static data members.

Listing 40-8.  Declaring and Defining Static Data Members

#include <iostream>
 
class rational {
public:
  rational();
  rational(int num);
  rational(int num, int den);
  int numerator() const { return numerator_; }
  int denominator() const { return denominator_; }
  // Some useful constants
  static const rational zero;
  static const rational one;
  static const rational pi;
private:
  void reduce(); //get definition from Listing 35-4
 
  int numerator_;
  int denominator_;
};
 
rational::rational() : rational{0, 1} {}
rational::rational(int num) : numerator_{num}, denominator_{1} {}
rational::rational(int num, int den)
: numerator_{num}, denominator_{den}
{
  reduce();
}
 
std::ostream& operator<<(std::ostream& out, rational const& r);
 
const rational rational::zero{};
const rational rational::one{1};
const rational rational::pi{355, 113};
 
int main()
{
  std::cout << "pi = " << rational::pi << ' ';
}

A static const data member with an integral type is a little odd, however. Only these data members can have an initializer inside the class definition, as part of the data member’s declaration. The value does not change the declaration into a definition, and you still must define the data member outside the class definition. However, by providing a value in the declaration, you can use the static const data member as a constant value elsewhere in the program, anywhere a constant integer is needed.

class string {

   static size_type const npos{-1};

};

Like other collection types, string declares size_type as a suitable integer type for representing sizes and indices. The implementation of the string class has to define this data member, but without an initial value.

string::size_type const string::npos;

Listing 40-9 shows some examples of static data members in a more sophisticated ID generator. This one uses a prefix as part of the IDs it produces and then uses a serial counter for the remaining portion of each ID. You can initialize the prefix to a random number to generate IDs that are unique even across multiple runs of the same program. (The code is not meant to show off a high-quality ID generator, only static data members.) Using a different prefix for every run is fine for production software but greatly complicates testing. Therefore, this version of the program uses the fixed quantity 1. A comment shows the intended code.

Listing 40-9.  Using Static Data Members for an ID Generator

#include <iostream>
 
class generate_id
{
public:
  generate_id() : counter_{0} {}
  long next();
private:
  short counter_;
  static short prefix_;
  static short const max_counter_ = 32767;
};
 
// Switch to random-number as the initial prefix for production code.
// short generate_id::prefix_(static_cast<short>(std::rand()));
short generate_id::prefix_{1};
short const generate_id::max_counter_;
 
long generate_id::next()
{
  if (counter_ == max_counter_)
    counter_ = 0;
  else
    ++counter_;
  return static_cast<long>(prefix_) * (max_counter_ + 1) + counter_;
}
 
int main()
{
  generate_id gen; // Create an ID generator
  for (int i{0}; i != 10; ++i)
    std::cout << gen.next() << ' ';
}

Declarators

As you’ve already seen, you can define multiple variables in a single declaration, as demonstrated in the following:

int x{42}, y{}, z{x+y};

The entire declaration contains three declarators. Each declarator declares a single name, whether that name is for a variable, function, or type. Most C++ programmers don’t use this term in everyday conversation, but C++ experts often do. You have to know official C++ terminology, so that if you have to ask for help from the experts, you can understand them.

The most important reason to know about separating declarations from definitions is so you can put a definition in one source file and a declaration in another. The next Exploration shows how to work with multiple source files.

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

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