Review of last time

Classes — encapsulating data and behavior.

class name {
  public:
    ... members ...

    ... methods ...
};

A class for dogs:

class dog {
  public:
    string name;
    string breed;
    bool tired;

    void eat() {
        cout << "Gulp!" << endl;
    }

    void speak() {
        cout << name << " says, Woof!" << endl;
    }

    void walk() {
        tired = true;
    }

    void nap() {
        if(tired) {
            tired = false;
            cout << "Zzz..." << endl;
        }
        else
            speak();
    }
};

Usage:

dog fido{"Fido", "Corgi", false};

fido.speak(); // Prints "Fido says, Woof!"
fido.rest();  // Prints "Fido says, Woof!";
fido.walk();
fido.rest();  // Prints "Zzz..."

A longer example:

Here’s a class with a members and some methods:

class post_office {
  public:

    vector<string> mailboxes;

    string check(int m) {
        return mailboxes.at(m);
    }

    bool has_message(int m) {
        return !mailboxes.at(m).empty();
    }

    void leave_message(int m, string message) {
        mailboxes.at(m) = message;
    }

    void add_mailbox() {
        mailboxes.push_back("No new mail"); // Should I do this?
    }

    int count_mailboxes() {
        return mailboxes.size();
    }
};

We can create multiple post_offices:

post_office school;
post_office work;
post_office home;

And then each one can have a different collection of mailboxes:

school.add_mailbox();
school.add_mailbox();
school.add_mailbox();

work.add_mailbox();

home.add_mailbox();
home.add_mailbox();

Aside: What am I doing? repeating myself! I should make a version that can add more than one new mailbox at a time!

// Overloaded version
void post_office::add_mailbox(int n = 1) {
    mailboxes.resize(mailboxes.size() + n, "");
}

So now we can just do

school.add_mailbox(3);
work.add_mailbox();
home.add_mailbox(2);

In the first batch, the current instance will be school, so it will school whose mailboxes are modified. Similarly, in the next section, it’s work, and after that it will be home.

And then we can store some messages:

school.leave_message(0, "Hey, what's up?");
school.leave_message(1, "Why are you late?");
school.leave_message(2, "Your order is ready.");

work.leave_message(0, "Reports are due");

home.leave_message(1, "Feed the cat.");

cout << school.check(1) << endl; // Prints "Why are you late?"

So now if I do

school.count_mailboxes()

what am I going to get? (3).

Declaring and defining methods

Although I’ve written all the method declarations inside the class definition so far, we actually have some flexibility in this regard.

  1. First, note that the usual rule for functions (they must be defined or declared before they can be used) is somewhat relaxed within class definitions. In particular, any method in a class definition can refer to any other method, regardless of the order of definition. (And similarly, any method can refer to any data member, even ones defined after it.)

    This is mostly done to simplify writing classes. It’s common to put the methods of a class at the top of the class definition, roughly in order of importance; similarly, data members often appear towards the bottom of a class definition. This would not be possible if methods/members were held to the string “must be defined before use” rule that applies to functions and variables outside classes.

    This means that we can structure a class like this:

     class city {
       public:
         void pickup_trash() {
           trash_collected++;
         }
    
       private:
         int trash_collected = 0;
     };
    
  2. As the above shows, we can give starting values for the data members. Every city that is created will start out having a trash_collected equal to 0. The rules for uninitialized data members are the same as for variables:

    Simple types like int, float and char start out with an undefined value. It’s just whatever happened to be in memory at that time. Giving an initial value (as I did above, for trash_collected) is always a good idea, just to make sure we know what’s in them.

    Class types like string and vector are “default initialized” which just means they start out empty (empty string, empty vector). This means you don’t need to initialize them unless you want them to have some specific value to start with.

  3. We don’t have to define every method within the class itself. We can put just the method declaration inside the class definition, and then put the method definition outside it, elsewhere in the file (or even in a different file, later on…):

     class whatever {
       public:
         void f(); // Declaration of f
         void g(); // Declaration of g
     };
    
     void whatever::g(); {
        f();
        cout << "Goodbye!";
     }
    
     void whatever::f() {
        cout << "Hello!";
     }
    

    The whatever::f syntax is how we tell C++ that the f we are defining is not a normal function, but is in fact part of the class whatever. C++ will look inside the class, see the matching declaration of f, and then link the definition to the declaration. Note that the method must be declared within the class; you cannot use whatever::h() to magically add a new method to whatever.

  4. Like functions, classes must be declared or defined before they are used. This means that our dog and owner classes must be defined like this

     class dog {
       public:
         ...
     };
    
     class owner {
       public:
         vector<dog> dogs;
     };
    

    because owner refers to dog. There is something corresponding to a “declaration” for classes, so that you can declare a class without giving it’s full definition, but it’s of much more limited use than a function declaration.

    A class declaration looks like this:

     class dog;
    

    and basically the only information it gives to C++ is that “dog is a class, which I’ll define later”. The problem is that this isn’t enough information for C++ to do anything really interesting with dogs. You can’t create a dog yet, because C++ doesn’t know what should go into it. You can’t create a vector<dog> because vector need to know how big dogs are, and it doesn’t know that yet. A class like dog that has a declaration but no definition yet is called an incomplete class. Pretty much the only thing you can do with an incomplete class is create a pointer to it:

     dog* puppy = new dog{...}; // OK
     dog  woofers{...};         // NOT OK, dog is incomplete
    

    So really, in order to do anything interesting with a class, you have to give its defintion, which means that you may have to put your classes definitions in a particular order, if they depend on each other.

Access control: private vs. public

Notice in the previous example that we never refer to .mailboxes outside of the methods of post_office. This is actually a good thing! It means that we’ve written our methods so as to properly encapsulate access to the data members. In fact, if we could somehow “hide” mailboxes so that only the methods of post_office were allowed to use it, that would be just fine. It would even be a little safer, as it would mean that outside users couldn’t get in and mess with the vector of mailboxes in weird ways.

For example, right now, any code outside of the post_office class itself can do

school.mailboxes.clear(); // PRANK! delete all mailboxes

and there’s nothing we can do about it.

We can fix this, however; we leave the methods as public but we’re going to make mailboxes private:

class post_office {
  public:

    string check(int m) {
        return mailboxes.at(m);
    }

    bool has_message(int m) {
        return !mailboxes.at(m).empty();
    }

    void leave_message(int m, string message) {
        mailboxes.at(m) = message;
    }

    void add_mailbox() {
        mailboxes.push_back("No mail yet!");
    }

    int count_mailboxes() {
        return mailboxes.size();
    }

  private:
    vector<string> mailboxes;
}

Anything after the public: has public access, you can use it from outside the post_office class. But the public section ends at private:; anything after private: has private access and can only be used from within the post_office class’s methods. This means that if we try to take this new version and do

post_office school;
// ...
school.mailboxes.clear(); // PRANK! delete all mailboxes

we will get a compile-time error; access to private member mailboxes is forbidden.

What do we mean by “outside” the class? Simple: functions (including main) or other code that is not part of the class definition. (Note that if you put a method declaration inside the class, and the definition of it outside, the definition still counts as being “inside” the class.) For example:

class thing {
  public:  
    int x = 0;

    void f() {
        cout << x; // OK, x is public
    }

    void g() {
        cout << y; // OK, y is private but g is inside thing
    }

    void h(); // Declaration only

  private:
    int y = 1;
};

void thing::h() {
    cout << x << y; // OK, h() is still inside thing
}

int main() {
    thing whatever;

    cout << whatever.x; // OK, x is public
    whatever.f();       // OK, f is public
    whatever.g();       // OK, g is public
    cout << whatever.y; // ERROR, y is private

    return 0;
}

Note, also, that data members and methods can be private. If a class has some methods that shouldn’t really be used by “outsiders”, only by the class itself, you can make them private to enforce that.

Access specifiers like public and private let you separate the parts of a class that other people are supposed to use, from the parts that are part of the “guts” of the class, and should not be messed with by outsiders. From the perspective of someone outside the class, using it, you know that you don’t need to worry about anything that is private. All you need to concern yourself with are the things that are public, because those are the only things you can use. Again, it’s another kind of abstraction, another way of hiding details that you don’t want to think about right now.

It’s common for a class to hide most or even all of its data members behind private, only exposing controlled access to them through public methods. The main reason for this is that a public data member cannot really be controlled: you cannot limit what values an outside user might assign into it, at any time. If you hide it behind some methods (e.g., a method for getting its value, and a method for setting its value) then you have complete control over what the outside world sees, and what happens when they try to change it.

When data members are hidden, it’s common to provide “access methods” for the members that control how they can be read and written. For example,

class employee {
  public: 
    string get_name() {
        return name;
    }

    void set_name(string new_name) {
        if(!new_name.empty())
            name = new_name;
    }

    int get_age() {
        return age;
    }    

  private:
    string name;
    int age;
    // etc.
};

Note that doing things this way gives the class an extra measure of control over how, and if, its data members can be accessed. E.g., above, we provide a method to change an employee‘s name, but only if the new name is not the empty string, and we do not provide any way to change an employee’s age.

private members cannot be accessed by functions outside the class, but sometimes we really want to write a function, outside a class, that accesses its private members. There are two ways to go about this:

A friend is a function, outside of a class, which has access to the class’s private members and methods. The class itself declares who its “friends” are, so you can’t just write a function and then make it a friend to gain access to things you shouldn’t be accessing. To declare a function a friend, copy the declaration of it into the class definition and put the keyword friend in front of it. For example:

void update_age(employee& e, int new_age);

class employee {
  public:
    //

  friend void update_age(employee& e, int new_age);

  private:
    string name;
    int age;
};

void update_age(employee& e, int new_age) {
    e.age = new_age; // OK, update_age is a friend of employee
}

Note that if you don’t say either of public or private, then everything inside a class defaults to being private:

class hidden {
    int x, y, z; // These are all private
};

...

hidden my_secret;
my_secret.x = 1; // ERROR, x is private

Polynomials, continued

Now that we have methods, we can clean up our polynomial class, moving some of the operations inside it, and making coeffs private.

class polynomial {
  public:

    void normalize();
    int degree();
    void shift(int powers);
    float evaluate(float x);
    void print();

    polynomial multiply(float s) {
        polynomial output = ???

        for(int i = 0; i < coeffs.size(); i++)
            output.coeffs.at(i) = coeffs.at(i) * s;

        return output;    
    }

    polynomial add(polynomial b) {
        int larger = max(degree(), b.degree());

        polynomial output = ???
        output.expand(larger); // Make room for coefficients

        for(int i = 0; i < larger; i++) {
            float x = i < coeffs.size() ? coeffs.at(i) : 0;
            float y = i < b.coeffs.size() ? b.coeffs.at(i) : 0;

            output.coeffs.at(i) = x + y;
        }

        return output;
    }

    void set(int i, float c) {
        if(degree() < i)
            coeffs.resize(i + 1);

        coeffs.at(i) = c;
    }

  private:
    vector<float> coeffs;
};

I’ve only writtten out some of the methods in full, and left most of them as declarations. I’ve also left out both the create function and put ???s in the lines where it was called. We’ll deal with the proper way to create objects in the next section.

Some general observations here:

Because add and multiply take two polynomials, and don’t “target” one or the other, it might make more sense to write them as normal functions, outside the class, and then just make them friends of the class.

this – The current instance

In add(float) I used *this. I mentioned that when we call a method on an instance, it magically knows what the current instance is. this is a pointer to the current instance. That is, whenever we refer to a member of method of the current instance, we are implicitly doing it through this. E.g.

class dog {
  public:
    string name;

    void speak() {
        cout << "Woof! says " << name << endl;
    }

    // is exactly equivalent to

    void speak() {
        cout << "Woof! says " << (*this).name << endl;
    }
}

If we need to actually get the current instance, the whole thing, we can get it through *this, which is what we do in add in order to make a copy of it.

Constructors: Encapsulating object creation

How can we add create to our polynomial class? Can we add it as a method? Let’s look at the prototype for create:

polynomial create(int degree);

Remember that functions which had one polynomial parameter turned into methods that had none. Functions that had two became methods with one, and so forth. What do we do with a function that already took no polynomial parameters? Another way to look at it is like this: methods can only be called if you have an existing instance to call them on. create is all about creating instances. So how will we call it on an instance, something.create(), without creating an instance something first? It’s a chicken-and-egg problem. The answer is that create turns into something different within polynomial: it becomes a constructor.

A constructor is a special part of a class that describes how instances of the class are created. It is responsible for setting up an instance, before it is used. Again, it’s all part of making the class responsible for its own objects: the class should handle setting up objects, and not rely on anything else to ensure that they are properly setup for it.

Inside a class definition, a constructor looks kind of like a method, except

A class can have more than one constructor, provided they take different numbers/types of arguments (i.e., constructors can be overloaded just like functions).

Constructors are usually public, but sometimes its useful to have a private constructor. (A class with nothing but private constructors is one that you can never create instances of!)

For polynomial, the constructor looks like this

class polynomial {
  public:

    polynomial(int degree) {
        coeffs.resize(degree + 1, 0.0);
    }

    ...
}

To use a constructor, we use the same syntax as to create a class instance by giving values for its members, only now we give values for the constructor’s parameters:

polynomial p{4}; // Creates a polynomial variable of degree 4 
polynomial p2 = polynomial{4}; // Same

Now we can finish the add and multiply methods:

    ...

    polynomial multiply(float s) {
        polynomial output{degree()};

        for(int i = 0; i < coeffs.size(); i++)
            output.coeffs.at(i) = coeffs.at(i) * s;

        return output;    
    }

    polynomial add(polynomial b) {
        int larger = max(degree(), b.degree());

        polynomial output{larger};

        for(int i = 0; i < larger; i++) {
            float x = i < coeffs.size() ? coeffs.at(i) : 0;
            float y = i < b.coeffs.size() ? b.coeffs.at(i) : 0;

            output.coeffs.at(i) = x + y;
        }

        return output;
    } 

For multiply, the output polynomial has the same degree as the current instance (we are not adding or removing any terms). For add, the resulting polynomial may be as big as the larger of the current instance and the parameter b.

Note that just as with methods, we can overload the constructor to provide different versions that take different arguments. In particular, a constructor that takes no arguments is called the “default constructor” and it’s usually a good idea to provide one, if you provide any constructor at all:

    polynomial() { }

The default constructor is called if you create an “unitialized” polynomial:

polynomial p; // Calls default constructor

(This should tell you that both string and vector have default constructors, and their default constructors set up the empty string and vector, respectively.)

One big caveat to note about providing constructors: Suppose we have a class

class point3d {
  public:
    float x,y,z;
};

Right now, we can create a point with

point3d p1{12,-5,3};

by just giving the values of x, y and z in the curly braces. However, if we add a constructor:

class point3d {
  public:
    point3d() { 
        // Does nothing
    }

    float x,y,z;
};

then we lose the ability to construct point3ds by just listing the values for their members:

point3d p2{-3,1,0}; // Error! no constructor takes 3 arguments

The idea is that the curly-braces-to-member-values is just a default provided by C++ if you don’t do anything; if you write a constructor, C++ assumes that you want to handle constructing the object yourself, and thus removes the default. Normally, if you’re going to write a constructor, you’re going to write all the constructors you might need. So for point3d we’d probably want to add

class point3d {
  public:
    point3d() { 
        // Leave values at 0
    }

    point3d(float f) {
        x = y = z = f;
    }

    point3d(float a, float b, float c) {
        x = a; y = b; z = c;
    }

    float x = 0, y = 0, z = 0;
};

Now we can do

point3d p1{1,2,3}; 

just like before, only now it’s using our constructor, rather than the built-in one we had before.

Constructors for conversions

Let’s give ourselves a constructor that takes a float and builds a constant polynomial (i.e., a polynomial of degree 0):

polynomial(float c) {
    coeffs.resize(1,c);
}

Now we have an easy way to construct polynomials that represent a single constant value: polynomial{1.2} is the polynomial

$$1.2x^0 + 0x^1 + 0x^2 + \cdots$$

Suppose we do this

polynomial p1;
p1.expand(3);
p1.set(0,1);
p1.set(1,2);
p1.set(2,3); 
// Now p1 = 1 + 2x + 3x^2

polynomial p2 = p1.add(12.0); // Huh?

Not only will this work, but afterwards

$$p_2 = 13.0 + 2x + 3x^2$$

I.e., 12.0 was treated like a constant polynomial. What’s going on? The answer is implicit conversion. Think about what happens when we do something like this:

void f(float);
...
f(1);

1 is an int, but it is implicitly converted to a float because that’s what f expects. polynomial::add expects another polynomial, so C++ tries to find a way to implicitly convert a float into a polynomial. And lo and behold, it finds our constructor:

polynomial(float c) { ... }

This means that we can also do things like this:

polynomial q = 12.0; 

and that will work just fine.

Another way of thinking of this constructor as, “here is how you transform a float into a polynomial”, which is exactly how C++ interprets it, using it to convert the float 12.0 into a polynomial which is then add-ed to p1 to get the result stored in p2.

In other words, a constructor that takes a single parameter of some type T also defines an implicit conversion from values of type T to the class’s type.

Sometimes this isn’t what we want. Think about the other constructor: if we had written p1.add(12) then it would have taken the int 12, passed it to the degree constructor, and constructed a 0 polynomial of degree 12, not what we were expecting. For constructors like this, that should never be used to perform conversions, we can mark them as explicit:

explicit polynomial(int degree) { ... }

Now C++ will never try to convert an int to a polynomial by using this constructor.

An exercise: polynomial multiplication

How do we multiply two arbitrary polynomials together? We’ll, it’s kind of like FOIL on steroids:

$$(1 + 2x + 3x^2) (-2 + 3x + x^2 + 4x^3)$$

We form all possible pairs of products from both sides, then match them up by the resulting powers of \(x\), and add up all the terms that have the same exponent.

(Draw grid representation)

We’re going to create a polynomial with a degree as big as could be needed: this is the sum of the two input polynomial’s degrees.

polynomial multiply(polynomial b) {
    polynomial output;
    output.expand(degree() + b.degreee());

    // ...
}

Then, we’re going to use a nested for-loop over all possible pairings. For each pair, we’ll compute the exponent and coefficient, and add it to that entry in the final polynomial. At the end, we’ll normalize to clean up any zeroes at the end.

polynomial multiply(polynomial b) {
    polynomial output;
    output.expand(degree() + b.degreee());

    for(int i = 0; i <= degree(); i++)
        for(int j = 0; j <= b.degree(); j++) {
            float c1 = coeffs.at(i);
            float c2 = b.coeffs.at(j);

            output.coeffs.at(i + j) += c1 * c2;
        }

    output.normalize();
    return output;
}