EXPLORATION 35

image

Access Levels

Everyone has secrets, some of us more than others. Classes have secrets too. For example: Throughout this book, you have used the std::string class without having any notion of what goes on inside the class. The implementation details are secrets—not closely guarded secrets, but secrets nonetheless. You cannot directly examine or modify any of string’s data members. Instead, it presents quite a few member functions that make up its public interface. You are free to use any of the publicly available member functions, but only the publicly available member functions. This Exploration explains how you can do the same with your classes.

Public vs. Private

The author of a class determines which members are secrets (for use only by the class’s own member functions) and which members are freely available for use by any other bit of code in the program. Secret members are called private, and the members that anyone can use are public. The privacy setting is called the access level. (When you read C++ code, you may see another access level, protected. I’ll cover that one later. Two access levels are enough to begin with.)

To specify an access level, use the private keyword or the public keyword, followed by a colon. All subsequent members in the class definition have that accessibility level until you change it with a new access-level keyword. Listing 35-1 shows the point class with access-level specifiers.

Listing 35-1.  The point Class with Access-Level Specifiers

struct point
{
public:
  point() : point{0.0, 0.0} {}
  point(double x, double y) : x_{x}, y_{y} {}
  point(point const&) = default;
 
  double x() const { return x_; }
  double y() const { return y_; }
 
  double angle()    const { return std::atan2(y(), x()); }
  double distance() const { return std::sqrt(x()*x() + y()*y()); }
 
  void move_cartesian(double x, double y)
  {
    x_ = x;
    y_ = y;
  }
  void move_polar(double r, double angle)
  {
    move_cartesian(r * std::cos(angle), r * std::sin(angle));
  }
 
  void scale_cartesian(double s)       { scale_cartesian(s, s); }
  void scale_cartesian(double xs, double ys)
  {
    move_cartesian(x() * xs, y() * ys);
  }
  void scale_polar(double r)           { move_polar(distance() * r, angle()); }
  void rotate(double a)                { move_polar(distance(), angle() + a); }
  void offset(double o)                { offset(o, o); }
  void offset(double xo, double yo)    { move_cartesian(x() + xo, y() + yo); }
 
private:
  double x_;
  double y_;
};

The data members are private, so the only functions that can modify them are point’s own member functions. Public member functions provide access to the position with the public x() and y() member functions.

image Tip  Always keep data members private, and provide access only through member functions.

To modify a position, notice that point does not let the user arbitrarily assign a new x or y value. Instead, it offers several public member functions to move the point to an absolute position or relative to the current position.

The public member functions let you work in Cartesian coordinates—that is, the familiar x and y positions, or in polar coordinates, specifying a position as an angle (relative to the x axis) and a distance from the origin. Both representations for a point have their uses, and both can uniquely specify any position in two-dimensional space. Some users prefer polar notation, while others prefer Cartesian. Neither user has direct access to the data members, so it doesn’t matter how the point class actually stores the coordinates. In fact, you can change the implementation of point to store the distance and angle as data members by changing only a few member functions. Which member functions would you have to change?

_____________________________________________________________

_____________________________________________________________

Changing the data members from x_ and y_ to r_ and angle_ necessitate a change to the x, y, angle, and distance member functions, just for access to the data members. You also have to change the two move functions: move_polar and move_cartesian. Finally, you have to modify the constructors. No other changes are necessary. Because the scale and offset functions do not access data members directly, but instead call other member functions, they are insulated from changes to the class implementation. Rewrite the point class to store polar coordinates in its data members. Compare your class with mine, which is shown in Listing 35-2.

Listing 35-2.  The point Class Changed to Store Polar Coordinates

struct point
{
public:
  point() : point{0.0, 0.0} {}
  point(double x, double y) : r_{0.0}, angle_{0.0} { move_cartesian(x, y); }
  point(point const& ) = default;
 
  double x() const { return distance() * std::cos(angle()); }
  double y() const { return distance() * std::sin(angle()); }
 
  double angle()    const { return angle_; }
  double distance() const { return r_; }
 
  void move_cartesian(double x, double y)
  {
    move_polar(std::sqrt(x*x + y*y), std::atan2(y, x));
  }
  void move_polar(double r, double angle)
  {
    r_ = r;
    angle_ = angle;
  }
 
  void scale_cartesian(double s)       { scale_cartesian(s, s); }
  void scale_cartesian(double xs, double ys)
  {
    move_cartesian(x() * xs, y() * ys);
  }
  void scale_polar(double r)           { move_polar(distance() * r, angle()); }
  void rotate(double a)                { move_polar(distance(), angle() + a); }
  void offset(double o)                { offset(o, o); }
  void offset(double xo, double yo)    { move_cartesian(x() + xo, y() + yo); }
 
private:
  double r_;
  double angle_;
};

One small difficulty is the constructor. Ideally, point should have two constructors, one taking polar coordinates and the other taking Cartesian coordinates. The problem is that both sets of coordinates are pairs of numbers, and overloading cannot distinguish between the arguments. This means you can’t use normal overloading for these constructors. Instead, you can add a third parameter: a flag that indicates whether to interpret the first two parameters as polar coordinates or Cartesian coordinates.

polar(double a, double b, bool is_polar)
{
  if (is_polar)
    move_polar(a, b);
  else
    move_cartesian(a, b);
}

It’s something of a hack, but it will have to do for now. Later in the book, you will learn cleaner techniques to accomplish this task.

class vs. struct

Exploration 34 hinted that the class keyword was somehow involved in class definitions, even though every example in this book so far uses the struct keyword. Now is the time to learn the truth.

The truth is quite simple. The struct and class keywords both start class definitions. The only difference is the default access level: private for class and public for struct. That’s all.

By convention, programmers tend to use class for class definitions. A common (but not universal) convention is to start class definitions with the public interface, tucking away the private members at the bottom of the class definition. Listing 35-3 shows the latest incarnation of the point class, this time defined using the class keyword.

Listing 35-3.  The point Class Defined with the class Keyword

class point
{
public:
  point() : r_{0.0}, angle_{0.0} {}
 
  double x() const { return distance() * std::cos(angle()); }
  double y() const { return distance() * std::sin(angle()); }
 
  double angle()    const { return angle_; }
  double distance() const { return r_; }
 
  ... other member functions omitted for brevity ...
 
private:
  double r_;
  double angle_;
};

Plain Old Data

So what good is the struct keyword? Authors of introductory books like it, because we can gradually introduce concepts such as classes without miring the reader in too many details, such as access levels, all at once. But what about real-world programs?

The struct keyword plays a crucial role in C compatibility. C++ is a distinct language from C, but many programs must interface with the C world. C++ has a couple of key features to interface with C. One of those features is POD. That’s right, POD, short for Plain Old Data.

The built-in types are POD. A class that has only public POD types as data members, with no constructors and no overloaded assignment operator, is a POD type. A class with a private member, a member with reference or other non-POD type, a constructor, or an assignment operator is not POD.

The importance of POD types is that legacy C functions in the C++ library, in third-party libraries, or operating-system interfaces require POD types. This book won’t go into the details of any of these functions, but if you find yourself having to call memcpy, fwrite, ReadFileEx, or any one of the myriad related functions, you will have to make sure you are using POD classes.

POD classes are often declared with struct, and the same header can be used in C and C++ programs. Even if you don’t intend to share the header with a C program, using struct in this way emphasizes to the reader that the class is POD. Not everyone uses struct to mean POD, but it’s a convention I follow in my own code. I use class for all other cases, to remind the human reader that the class can take advantage of C++ features and is not required to maintain compatibility with C.

Public or Private?

Usually, you can easily determine which members should be public and which should be private. Sometimes, however, you have to stop and ponder. Consider the rational class (last seen in Exploration 33). Rewrite the rational class to take advantage of access levels.

Did you decide to make reduce() public or private? I chose private, because there is no need for any outside caller to call reduce(). Instead, the only member functions to call reduce() are the ones that change the data members themselves. Thus, reduce() is hidden from outside view and serves as an implementation detail. The more details you hide, the better, because it makes your class easier to use.

When you added access functions, did you let the caller change the numerator only? Did you write a function to change the denominator only? Or did you ask that the user assign both at the same time? The user of a rational object should treat it as a single entity, a number. You can’t assign only a new exponent to a floating-point number, and you shouldn’t be able to assign only a new numerator to a rational number. On the other hand, I see no reason not to let the caller examine only the numerator or only the denominator. For example, you may want to write your own output formatting function, which requires knowing the numerator and denominator separately.

A good sign that you have made the right choices is that you can rewrite all the operator functions easily. These functions should not have to access the data members of rational, but use only the public functions. If you tried to access any private members, you learned pretty quickly that the compiler wouldn’t let you. That’s what privacy is all about.

Compare your solution with my solution, presented in Listing 35-4.

Listing 35-4.  The Latest Rewrite of the rational Class

#include <cassert>
#include <cstdlib>
#include <iostream>
#include <sstream>
 
/// Compute the greatest common divisor of two integers, using Euclid’s algorithm.
int gcd(int n, int m)
{
  n = std::abs(n);
  while (m != 0) {
    int tmp(n % m);
    n = m;
    m = tmp;
  }
  return n;
}
 
/// Represent a rational number (fraction) as a numerator and denominator.
class rational
{
public:
  rational(): rational{0}  {}
  rational(int num): numerator_{num}, denominator_{1} {} // no need to reduce
  rational(rational const&) = default;
  rational(int num, int den)
  : numerator_{num}, denominator_{den}
  {
    reduce();
  }
 
  rational(double r)
  : rational{static_cast<int>(r * 10000), 10000}
  {
    reduce();
  }
 
  int numerator()   const { return numerator_; }
  int denominator() const { return denominator_; }
  float to_float()
  const
  {
    return static_cast<float>(numerator()) / denominator();
  }
 
  double to_double()
  const
  {
    return static_cast<double>(numerator()) / denominator();
  }
 
  long double to_long_double()
  const
  {
    return static_cast<long double>(numerator()) /
           denominator();
  }
 
  /// Assign a numerator and a denominator, then reduce to normal form.
  void assign(int num, int den)
  {
    numerator_ = num;
    denominator_ = den;
    reduce();
  }
private:
  /// Reduce the numerator and denominator by their GCD.
  void reduce()
  {
    assert(denominator() != 0);
    if (denominator() < 0)
    {
      denominator_ = -denominator();
      numerator_ = -numerator();
    }
    int div{gcd(numerator(), denominator())};
    numerator_ = numerator() / div;
    denominator_ = denominator() / div;
  }
 
  int numerator_;
  int denominator_;
};
 
/// Absolute value of a rational number.
rational abs(rational const& r)
{
  return rational{std::abs(r.numerator()), r.denominator()};
}
 
/// Unary negation of a rational number.
rational operator-(rational const& r)
{
  return rational{-r.numerator(), r.denominator()};
}
 
/// Add rational numbers.
rational operator+(rational const& lhs, rational const& rhs)
{
  return rational{
          lhs.numerator() * rhs.denominator() + rhs.numerator() * lhs.denominator(),
          lhs.denominator() * rhs.denominator()};
}
 
/// Subtraction of rational numbers.
rational operator-(rational const& lhs, rational const& rhs)
{
  return rational{
          lhs.numerator() * rhs.denominator() - rhs.numerator() * lhs.denominator(),
          lhs.denominator() * rhs.denominator()};
}
 
/// Multiplication of rational numbers.
rational operator*(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator()};
}
 
/// Division of rational numbers.
/// TODO: check for division-by-zero
rational operator/(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator() * rhs.denominator(),
                  lhs.denominator() * rhs.numerator()};
}
 
/// Compare two rational numbers for equality.
bool operator==(rational const& a, rational const& b)
{
  return a.numerator() == b.numerator() and a.denominator() == b.denominator();
}
 
/// Compare two rational numbers for inequality.
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}
/// Compare two rational numbers for less-than.
bool operator<(rational const& a, rational const& b)
{
  return a.numerator() * b.denominator() < b.numerator() * a.denominator();
}
 
/// Compare two rational numbers for less-than-or-equal.
inline bool operator<=(rational const& a, rational const& b)
{
  return not (b < a);
}
/// Compare two rational numbers for greater-than.
inline bool operator>(rational const& a, rational const& b)
{
  return b < a;
}
 
/// Compare two rational numbers for greater-than-or-equal.
inline bool operator>=(rational const& a, rational const& b)
{
  return not (b > a);
}
 
/// Read a rational number.
/// Format is @em integer @c / @em integer.
std::istream& operator>>(std::istream& in, rational& rat)
{
  int n{}, d{};
  char sep{};
  if (not (in >> n >> sep))
    // Error reading the numerator or the separator character.
    in.setstate(in.failbit);
  else if (sep != '/')
  {
    // Push sep back into the input stream, so the next input operation
    // will read it.
    in.unget();
    rat.assign(n, 1);
  }
  else if (in >> d)
    // Successfully read numerator, separator, and denominator.
    rat.assign(n, d);
  else
    // Error reading denominator.
    in.setstate(in.failbit);
 
  return in;
}
 
/// Write a rational numbers.
/// Format is @em numerator @c / @em denominator.
std::ostream& operator<<(std::ostream& out, rational const& rat)
{
  std::ostringstream tmp{};
  tmp << rat.numerator() << '/' << rat.denominator();
  out << tmp.str();
 
  return out;
}

Feel free to save Listing 35-4 as rational.hpp, so you can use it and reuse in your own programs. You will revisit this class as you learn more advanced features of C++.

Classes are one of the fundamental building blocks of object-oriented programming. Now that you know how classes work, you can see how they apply to this style of programming, which is the subject of 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