This is day 03 of the Two weeks of C++ series. Today is about modularity.
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:
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.
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.
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.
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.
Refer to the References section below for a reminder.
If a function should not throw any errors, you can declare it as a
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.
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
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:
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:
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.
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:
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.
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:
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
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
- 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
Here’s a good tutorial on C++ exceptions and some standard exception types: TutorialsPoint