Functions and Methods

When programming, it’s helpful to make our code neat and organized. That allows us to easily find where the mistakes are. By now, you might have realized that haphazardly indenting your code makes it difficult to spot syntax errors when you mismatch brackets. And while C++ doesn’t care about whitespace, none of us put everything on one line because that would be too difficult to debug.

There are other ways to improve your code quality too. Let’s say we want to do some more things with fractions and want to compare two fractions. We would then have code that looks something like this:

  1. struct fraction {
  2. int numerator;
  3. int denominator;
  4. };
  5. fraction fraction_new(int numerator, int denominator) {
  6. fraction f;
  7. f.numerator = numerator;
  8. f.denominator = denominator;
  9. return f;
  10. }
  11. bool fraction_less_than(fraction* first, fraction* second) {
  12. if (first->numerator * second->denominator < second->numerator * first->denominator) {
  13. return 1;
  14. }
  15. return 0;
  16. }
  17. bool fraction_equals(fraction* first, fraction* second) {
  18. // Two things are equal if neither a < b nor b < a are true
  19. if (fraction_less_than(first, second) || fraction_less_than(second, first)) {
  20. return 0;
  21. }
  22. return 1;
  23. }
  24. bool fraction_greater_than(fraction* first, fraction* second) {
  25. // a > b if b < a
  26. if(fraction_less_than(second, first)) {
  27. return 1;
  28. }
  29. return 0;
  30. }
  31. main() {
  32. fraction a = fraction_new(1, 2);
  33. fraction b = fraction_new(3, 4);
  34. printf("a < b is %d", fraction_less_than(a, b));
  35. }

This is good. But we can do better through a novel way of thinking called object-oriented programming. In object-oriented programming, the core idea is that you have objects that can do actions. What we mean is that in the code above, we have functions that operate on or modify our objects. In object-oriented programming, we think of the objects as being able to perform actions instead of actions being performed on them. In this case, we want our fraction to be able to compare itself with another fraction, so we can end up with code that looks like this:

  1. main() {
  2. fraction a = fraction_new(1, 2);
  3. fraction b = fraction_new(3, 4);
  4. printf("a < b is %d", a.less_than(b));
  5. }

What is the advantage? In the case that we have multiple implementations of fractions, say, one internally storing int and another internally storing long long, we would not have to replace all the instances of fraction_less_than with long_long_fraction_less_than. We just have to change the type declaration on fraction a to long_long_fraction a, and the compiler will take care of handling all of the cases of a.less_than. The member functions or "actions" we’re talking about are called methods in the literature. So when reading outside material, it’s common to find a phrase such as "an object supports the less_than method" or "an object implements the less_than method".

There are even more advantages to object-oriented programming. Suppose there were a library out there that knows how to work with objects that support the less_than method, then we be able to directly use everything in that library as long as we implement the method required. And there are plenty of things you can do with just less_than, for example. We can take the minimum of two objects through that. We can sort arrays. Find the largest item in the array less than some value. There’s a lot more you can do with just that. So simply implementing one method allows your new data type to use common libraries. In the next section, we will see how to use custom objects with the standard library.

A third advantage to object-oriented programming is that it allows us to think of objects as actors, meaning things that do things, and not just things that group variables together. If you have a string object, your string can implement methods that search for substrings. A set-like object can implement methods that check if an item is an element of the set. And objects line cin can implement methods like getline that reads input from some some data source.

It’s time we look at some sample code that defines methods.

  1. #include <stdio.h>
  2. struct fraction {
  3. int numerator;
  4. int denominator;
  5. bool less_than(fraction* second) {
  6. // `this` is a special keyword/pointer that refers to the object doing the action
  7. if (this->numerator * second->denominator < second->numerator * this->denominator) {
  8. return 1;
  9. }
  10. return 0;
  11. }
  12. bool equals(fraction* second) {
  13. // Two things are equal if neither a < b nor b < a are true
  14. if (this->less_than(second) || second->less_than(this)) {
  15. return 0;
  16. }
  17. return 1;
  18. }
  19. bool greater_than(fraction* second) {
  20. if(second->less_than(this)) {
  21. return 1;
  22. }
  23. return 0;
  24. }
  25. };
  26. fraction fraction_new(int numerator, int denominator) {
  27. fraction f;
  28. f.numerator = numerator;
  29. f.denominator = denominator;
  30. return f;
  31. }
  32. main() {
  33. fraction a = fraction_new(1, 2);
  34. fraction b = fraction_new(3, 4);
  35. printf("a < b is %d", a.less_than(&b));
  36. }

The only important thing to remember is that you can define functions inside the struct definition, and those will become methods. Then inside those functions you will have access to a magic keyword this which is a pointer to the object doing the action.