Two Weeks of C++ - Day 03

Intro

This is day 03 of the Two weeks of C++ series. Today is about modularity.

If it’s your first time here, please, read the disclaimers before moving on.

Separate Compilation

Separate compilation involves separating, at a language level, the interface (the declaration of a module) from the implementation (the definition of that module).

Notice how we’re using the same terms (interface and implementation) we used for the public and private members of a class from the last day as the declaration and definition of a class here? Yeah, that’s why I mentioned at a language level here. Keep this in mind!

For example, it would involve declaring a whole class in a header (.h) file, define that class in a respectively named source (.cpp) file and then use that class from another source file:

Separate Compilation: The interfaceVector.h
1
2
3
4
5
6
7
8
9
10
class Vector
{
public:
Vector(int s);
double& operator[](int i);
int size();
private:
double∗ elem;
int sz;
};
Separate Compilation: The implementationVector.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Vector.h"
Vector::Vector(int s) :elem{new double[s]}, sz{s}
{
}
double& Vector::operator[](int i)
{
return elem[i];
}
int Vector::size()
{
return sz;
}
Separate Compilation: Usagedoing_stuff_with_Vector.cpp
1
2
3
4
5
6
7
8
9
10
11
12
#include "Vector.h"
#include <cmath>
using namespace std;
double sqrt_sum(Vector& v)
{
double sum = 0;
for (int i = 0; i != v.size(); ++i)
sum += sqrt(v[i]);
return sum;
}

This is another well known advice among 42 students. I mentioned it here because it’s a really important practice when it comes to modularity. It saves compilation time and reduces possible errors.

Namespaces

In C++, namespaces are a great way to “package” some declarations together so that they don’t clash with other names in the current scope.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
void print_something()
{
cout << "something" << endl;
}
namespace My_code
{
void print_something()
{
cout << "Something else" << endl;
}
}
int main()
{
print_something();
My_code::print_something();
return 0;
}

A great use of namespaces would have been my Libft library. I wish C had this feature (no hacks ;) ), would have saved me from having to put an ft_ in front of each function name.

Error Handling

It is a good idea to design and articulate a strategy for error handling early on in the development of a program.

~ Bjarne Stroustrup

Error handling is a crucial part of software development.

Exceptions

I won’t go into too much detail about how to handle errors in C++ using exceptions because it’s basically the same thing as in JavaScript.

Refer to the References section below for a reminder.

The noexcept specifier

If a function should not throw any errors, you can declare it as a noexcept function:

1
2
3
4
5
6
void fill_up() noexcept
{
std::list<int> l(10);
std::iota(l.begin(), l.end(), -4);
// ...
}

Using this specifier is a result of good intent and planning. If the function still throws an error (bad planning), the standard-library function terminate is called to immediately terminate the program.

Invariants

Whenever we define a function, we should consider what its preconditions are and if feasible test them.

~ Bjarne Stroustrup

To understand invariants, let’s see a good example of error handling. Yeah, I know I said I wouldn’t go into too much detail :P

In our Vector class definition above, the subscript operator assumes the integer i that it receives is within the bounds of the elem property. What if it wasn’t? So let’s make sure we throw an error when it’s not the case:

1
2
3
4
5
6
double& Vector::operator[](int i)
{
if (i < 0 || size() <= i )
throw out_of_range{"Vector::operator[]"};
return elem[i];
}

Here we’re throwing an out_of_range error, which is an actual type from the standard library (<stdexcept>) used by some standard-library container access functions. Here’s how a user of that class would catch the error:

1
2
3
4
5
6
7
8
9
10
11
void f(Vector& v)
{
// ...
try {
v[v.size()] = 7; // try to access beyond the end of v
}
catch (out_of_range) { // oops: out_of_range error
// ... handle out of range error ...
}
// ...
}

Okay, that’s good, we took care of this possible error. Remember, the subscript operator depends on the values of the elem prop, therefor, it depends on the constructor.

Now, if we look at the constructor, it’s assuming that the integer s is always a positive number and so it allocates memory for the elem property. This is what an invariant is, it’s simply a statement that is assumed to be true for a class.

In our Vector class definition above, the constructor is allocating the memory for the elem prop, but it’s not establishing the invariant for its class, which is to check that the integer s passed to it is actually a positive number. Let’s take care of that:

1
2
3
4
5
6
7
Vector::Vector(int s)
{
if (s < 0)
throw length_error{};
elem = new double[s];
sz = s;
}

Now that’s better! If we pass, for example, -42 to our constructor, it’s going to complain about it and we know how to catch that error. length_error is yet another standard exception type.

Static Assertions

Exceptions are great for finding run time errors. It’s also good practice to handle some errors at compile time whenever possible. This is when static assertions come into play.

A really basic example of it would be to check if the integers on a system are at least 4 bytes:

static_assert(4 <= sizeof(int), "Integers are too small");

If the first parameter of static_assert is false, it prints its second parameter as a compiler error message. Neat, right?

And of course, we can use it when it comes to constant expressions:

1
2
3
4
5
6
constexpr unsigned int nick_age = 21;
const unsigned int mom_age = 42;
unsigned int dad_age = 42;
static_assert(mom_age <= nick_age, "Cannot be older than your mother!"); // OK, constant expressions used
static_assert(dad_age <= nick_age, "Cannot be older than your father!"); // KO, dad_age is not a constant expression

Not a really useful example, but hey, you get it! Those are not real age numbers by the way.

Static assertions will be more useful when we need to make assertions about types used as parameters in generic programming. More about this in days 5 and 11.

One effect of modularity and abstraction (in particular, the use of libraries) is that the point where a run-time error can be detected is separated from the point where it can be handled.

~ Bjarne Stroustrup

Conclusion

Today’s subject is definitely not going to be useful for competitive programming. But I’m always in favor of good practices, so this is going to be great when it comes to software engineering.

Here’s what to remember for the day:

  • Distinguish between declarations (used as interfaces) and definitions (used as implementations)
  • Use header files to represent interfaces and to emphasize logical structure
  • Avoid non-inline function definitions in headers
  • Use namespaces to express logical structure
  • Develop an error-handling strategy early in a design
  • Use purpose-designed user-defined types as exceptions (not built-in types)
  • Don’t try to catch every exception in every function
  • If your function may not throw, declare it noexcept
  • Let a constructor establish an invariant, and throw if it cannot
  • Design your error-handling strategy around invariants
  • What can be checked at compile time is usually best checked at compile time (using static_assert)

References

Here’s a good tutorial on C++ exceptions and some standard exception types: TutorialsPoint