A few months back, I learned about defensive programming at a local C++ meetup. The idea took a little while to sink in. I now rarely write any code without using defensive programming in some way or another. However, it does become tedious to write, especially if you write good error messages. Before delving too deep into the subject, lets discuss the basics.
What Is Defensive Programming
I don’t claim to be an expert on the subject (I’ve only started using it recently), but here is a short explanation of the concept as I understand it. Lets say you have a class, the_mighty_potato
. You use the rule of 0 for its constructor generation, as it is the best rule.
struct the_mighty_potato {
int carbs{ 42 };
};
Years pass, you have a kid, live a lovely life and someday a coworker (not you of course) adds a constructor which does something fancy to the class.
struct the_mighty_potato {
// very skillz
the_mighty_potato(int some_number, int mul)
: carbs(some_number * mul) {
}
int carbs{ 42 };
};
Your mighty potato class has just lost its compiler generated default constructor. In this particular example, the problem is quite obvious. However, the error messages may not be so clear, especially if you don’t know much about the implicitly-declared constructors. In some cases, more subtle changes can be hard to track down. This sort of “silent” class modification affects types that rely on a trivial copy constructors, trivial destructors and the nothrow move constructor.
C++’s implicitly-declared constructor rules can get tricky. I would be lying if I stated I remember every detail of every rule. I just don’t. From my experience “in the real world”, these rules aren’t well understood, if understood at all. I don’t find that particularly surprising, as everything is hidden and implicit. It is difficult to learn something invisible. What if there was a way to actually learn these rules in a programmer friendly way? At the very least, leaving your constructor generation up to chance is a bad idea.
Intent is also paramount for readable and understandable code. If I write a vec3
class to store vertices of a 3d mesh, I know it will end up in a vector-like container at some point in the codebase. While writing this class, I want to explicitly state the intent. “This is a small class that will be fast when stored in a vector.” This means making sure the class has a trivial copy constructor so vector::resize
can use memcpy
. So one should use = default
then, right? Bingo bango Bob’s your uncle.
struct vec3 {
vec3(const vec3&) = default;
float x{ 0.f };
float y{ 0.f };
float z{ 0.f };
};
Oh, if it were that simple. = default
is just a suggestion. Your compiler may stop generating a constructor, silently. For example, if you later decide to inherit from vec3, you may change the destructor to be virtual. Without knowing, your “small and fast” class cannot be memcopied during vector growth. Not so small and fast anymore… Well OK, in this case it is still small and pretty fast, but you get the idea.
struct vec3 {
virtual ~vec3() {
}
vec3(const vec3&) = default; // Nope
float x{ 0.f };
float y{ 0.f };
float z{ 0.f };
};
Defensive programming fixes these “gotchas”.
Defensive Programming 101
OK, enough talking, lets dig into it. Most of you probably already know how you can defend yourself from the above issues. Using type_traits
of course! Lets guard our mighty potato class using simple static_asserts
.
#include <type_traits>
#include <memory>
struct the_mighty_potato {
the_mighty_potato(int some_number, int mul)
: carbs(some_number * mul) {
}
int carbs{ 42 };
};
static_assert(std::is_default_constructible_v<the_mighty_potato>,
"the_mighty_potato : must be default constructible"); // Fail with a nice error message!
And our previous vec3
example.
struct vec3 {
virtual ~vec3() {
}
vec3(const vec3&) = default;
float x{ 0.f };
float y{ 0.f };
float z{ 0.f };
};
static_assert(std::is_trivially_copy_constructible_v<vec3>,
"vec3 : must be trivially copy constructible"); // Fails
These asserts guard your classes from inadvertent mistakes. The error message is very clear and it makes intent clear as well. If I open a library header with such asserts, I instantly understand the general characteristics of a given class. This is huge. The technique also helps reworking old code. It will help you remember what you were thinking about when you wrote a particular piece of code. Finally, it will safe-guard junior developers from common pitfalls and may result in some great questions from them as well. Be prepared to explain the intricacies of implicitly-declared constructors.
The static_assert Problem
As is often the case, the problem with the aforementioned static_asserts
is laziness, often referred to as “overhead”. It gets really tiresome writing all those asserts for every class you use. Lets take “rule of 5” as an example.
struct the_mighty_potato {
int carbs{ 42 };
};
static_assert(std::is_destructible_v<the_mighty_potato>,
"the_mighty_potato : must be destructible");
static_assert(std::is_copy_constructible_v<the_mighty_potato>,
"the_mighty_potato : must be copy constructible");
static_assert(std::is_move_constructible_v<the_mighty_potato>,
"the_mighty_potato : must be move constructible");
static_assert(std::is_copy_assignable_v<the_mighty_potato>,
"the_mighty_potato : must be copy assignable");
static_assert(std::is_move_assignable_v<the_mighty_potato>,
"the_mighty_potato : must be move assignable");
I couldn’t imagine trying to convince coworkers to use this. The adoption friction is simply too high.
Creating Rules With constexpr Functions
One can use constexpr
functions to encapsulate static_asserts
. You can call this function from another static_assert
and everything will work as expected. I’ve personally used this in a library of mine to great success. I came up with the following rules.
fulfills_rule_of_5
: destructible, copy constructible, move constructible, copy assignable, move assignable.fulfills_rule_of_6
: rule of 5 with an added check for a default constructor.fulfills_fast_vector
: either trivially copy constructible and trivially destructible, or nothrow move constructible.fulfills_move_only
: no copy constructor and no copy assignement, has move constructor and assignement.
An example implementation for the famous rule of 5.
template <class T>
constexpr bool fulfills_rule_of_5() {
static_assert(std::is_destructible_v<T>, "T : must be destructible");
static_assert(std::is_copy_constructible_v<T>, "T : must be copy constructible");
static_assert(std::is_move_constructible_v<T>, "T : must be move constructible");
static_assert(std::is_copy_assignable_v<T>, "T : must be copy assignable");
static_assert(std::is_move_assignable_v<T>, "T : must be move assignable");
return std::is_destructible_v<T> && std::is_copy_constructible_v<T>
&& std::is_move_constructible_v<T> && std::is_copy_assignable_v<T>
&& std::is_move_assignable_v<T>;
}
struct the_mighty_potato {
the_mighty_potato(int some_number, int mul)
: carbs(some_number * mul) {
}
int carbs{ 42 };
std::unique_ptr<int> calories{ nullptr };
};
static_assert(fulfills_rule_of_5<the_mighty_potato>(),
"the_might_potato : must fulfill rule of 5"); // Fails
It works fine but isn’t perfect. I have 2 issues with this solution.
First, the error messages inside the function aren’t as clear. Since static_asserts
need a string literal error message, there doesn’t seem to be a way to specify the type T
as a string. I’ve tried with boost.hana like compile-time strings, but have failed to make it work.
The other issue is the error message you need write in the parent static_assert
. After using this on many classes, it does become really tedious. It is yet another barrier to adoption.
Creating Rules With MACROs
Of course, it had to come down to this. We will need to use macros to make a highly usable, low friction defensive library. Feel free to use the constexpr
versions if you are allergic to macros, I fully understand.
This solution is based off an essential feature of static_assert
. You can use multiple string literals seperated by spaces. For example, one can write static_assert(std::is_same_v<unsigned, int>, "they " "aren't " "the " "same");
We can use a stringify macro to use class names in our error messages. To stringify a macro argument, one simply prepends #
in front of the argument name. For example, if I have a macro POTATO_CRUSHER(x)
, the argument x
can be surrounded by quotes using #x
inside the macro.
#define POTATO_CRUSHER(x) #x
const char* text = POTATO_CRUSHER(russet) // Expands to "russet"
We can use this feature to build an extremely simple and compelling to use defensive library. Our final defensive macro call will look like this.
struct the_mighty_potato {
int carbs{ 42 };
};
FULFILLS_RULE_OF_5(the_mighty_potato);
We will be compositing different C++ trait macros into higher level rules to guard our classes from silent breaks. First, lets define a few macros needed for the rule of 5 check. We use a lambda to encapsulate a static_assert
with a nice error message thanks to stringification. We also return the result which will be used for the composited guard rule and its own error message.
Note : For the rest of this post, I will prepend the macros with FEA_
. Since macros leak everywhere, I try to minimize conflicts with C-like namespaces. fea
is my library’s namespace.
#define FEA_COPY_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_copy_constructible_v<t>, \
#t " : must be copy constructible"); \
return std::is_copy_constructible_v<t>; \
}
#define FEA_COPY_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_copy_assignable_v<t>, \
#t " : must be copy assignable"); \
return std::is_copy_assignable_v<t>; \
}
#define FEA_MOVE_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_move_constructible_v<t>, \
#t " : must be move constructible"); \
return std::is_move_constructible_v<t>; \
}
#define FEA_MOVE_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_move_assignable_v<t>, \
#t " : must be move assignable"); \
return std::is_move_assignable_v<t>; \
}
#define FEA_DESTRUCTIBLE(t) \
[]() { \
static_assert( \
std::is_destructible_v<t>, #t " : must be destructible"); \
return std::is_destructible_v<t>; \
}
With these building blocks ready, we can now create a rule of 5 check. We combine all desired macro traits, and give a high-level error message which will make things quite clear when the rule fails.
#define FEA_FULFILLS_RULE_OF_5(t) \
static_assert(FEA_DESTRUCTIBLE(t)() && FEA_COPY_CONSTRUCTIBLE(t)() \
&& FEA_MOVE_CONSTRUCTIBLE(t)() && FEA_COPY_ASSIGNABLE(t)() \
&& FEA_MOVE_ASSIGNABLE(t)(), \
#t " : doesn't fulfill rule of 5")
Using this macro is simple, fast and the error messages are very clear.
struct tssk_tssk {
~tssk_tssk() {
delete t;
}
tssk_tssk(tssk_tssk&& tssk) noexcept {
if (this == &tssk)
return;
t = tssk.t;
tssk.t = nullptr;
}
int* t = new int(42);
};
FEA_FULFILLS_RULE_OF_5(tssk_tssk); // Fails
Which looks like this in your IDE.
Some rules like move-only require a trait fail. You will need to create “NOT” macros (examples below).
The fast vector check presented in the final example is somewhat limiting. Since MSVC doesn’t provide a C++11 is_trivially_copyable
(and it is broken on other compilers as well), I check whether an object is trivially_copy_constructible
instead. I also assume you object is trivially_destructible
. If not, I enforce nothrow_move_constructible
.
Hopefully defensive programming helps clarify and harden your code base as it did mine. A full example follows, a header version is also hosted on github.
Many thanks to Gabriel Aubut-Lussier for his amazing work organizing our local c++ meetup group and for teaching me the basics of defensive programming.
defensive.hpp
#pragma once
#include <type_traits>
#define FEA_DEFAULT_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_default_constructible_v<t>, \
#t " : must be default constructible"); \
return std::is_default_constructible_v<t>; \
}
#define FEA_TRIVIALLY_DEFAULT_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_trivially_default_constructible_v<t>, \
#t " : must be trivially default constructible"); \
return std::is_trivially_default_constructible_v<t>; \
}
#define FEA_COPY_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_copy_constructible_v<t>, \
#t " : must be copy constructible"); \
return std::is_copy_constructible_v<t>; \
}
#define FEA_NOT_COPY_CONSTRUCTIBLE(t) \
[]() { \
static_assert(!std::is_copy_constructible_v<t>, \
#t " : must not be copy constructible"); \
return !std::is_copy_constructible_v<t>; \
}
#define FEA_TRIVIALLY_COPY_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_trivially_copy_constructible_v<t>, \
#t " : must be trivially copy constructible"); \
return std::is_trivially_copy_constructible_v<t>; \
}
#define FEA_COPY_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_copy_assignable_v<t>, \
#t " : must be copy assignable"); \
return std::is_copy_assignable_v<t>; \
}
#define FEA_NOT_COPY_ASSIGNABLE(t) \
[]() { \
static_assert(!std::is_copy_assignable_v<t>, \
#t " : must not be copy assignable"); \
return !std::is_copy_assignable_v<t>; \
}
#define FEA_TRIVIALLY_COPY_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_trivially_copy_assignable_v<t>, \
#t " : must be trivially copy assignable"); \
return std::is_trivially_copy_assignable_v<t>; \
}
#define FEA_MOVE_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_move_constructible_v<t>, \
#t " : must be move constructible"); \
return std::is_move_constructible_v<t>; \
}
#define FEA_TRIVIALLY_MOVE_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_trivially_move_constructible_v<t>, \
#t " : must be trivially move constructible"); \
return std::is_trivially_move_constructible_v<t>; \
}
#define FEA_NOTHROW_MOVE_CONSTRUCTIBLE(t) \
[]() { \
static_assert(std::is_nothrow_move_constructible_v<t>, \
#t " : must be nothrow move constructible"); \
return std::is_nothrow_move_constructible_v<t>; \
}
#define FEA_MOVE_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_move_assignable_v<t>, \
#t " : must be move assignable"); \
return std::is_move_assignable_v<t>; \
}
#define FEA_TRIVIALLY_MOVE_ASSIGNABLE(t) \
[]() { \
static_assert(std::is_trivially_move_assignable_v<t>, \
#t " : must be trivially move assignable"); \
return std::is_trivially_move_assignable_v<t>; \
}
#define FEA_DESTRUCTIBLE(t) \
[]() { \
static_assert( \
std::is_destructible_v<t>, #t " : must be destructible"); \
return std::is_destructible_v<t>; \
}
#define FEA_TRIVIALLY_DESTRUCTIBLE(t) \
[]() { \
static_assert(std::is_trivially_destructible_v<t>, \
#t " : must be trivially destructible"); \
return std::is_trivially_destructible_v<t>; \
}
#define FEA_TRIVIALLY_COPYABLE(t) \
[]() { \
static_assert(std::is_trivially_copyable_v<t>, \
#t " : must be trivially copyable"); \
return std::is_trivially_copyable_v<t>; \
}
#define FEA_FULFILLS_RULE_OF_5(t) \
static_assert(FEA_DESTRUCTIBLE(t)() && FEA_COPY_CONSTRUCTIBLE(t)() \
&& FEA_MOVE_CONSTRUCTIBLE(t)() && FEA_COPY_ASSIGNABLE(t)() \
&& FEA_MOVE_ASSIGNABLE(t)(), \
#t " : doesn't fulfill rule of 5")
#define FEA_FULFILLS_RULE_OF_6(t) \
static_assert(FEA_DESTRUCTIBLE(t)() && FEA_DEFAULT_CONSTRUCTIBLE(t)() \
&& FEA_COPY_CONSTRUCTIBLE(t)() \
&& FEA_MOVE_CONSTRUCTIBLE(t)() && FEA_COPY_ASSIGNABLE(t)() \
&& FEA_MOVE_ASSIGNABLE(t)(), \
#t " : doesn't fulfill rule of 6")
// is_trivially_copyable broken everywhere
#define FEA_FULFILLS_FAST_VECTOR(t) \
static_assert((std::is_trivially_copy_constructible_v< \
t> && std::is_trivially_destructible_v<t>) \
|| std::is_nothrow_move_constructible_v<t>, \
#t " : doesn't fulfill fast vector (trivially copy constructible " \
"and trivially destructible, or nothrow move constructible)")
#define FEA_FULFILLS_MOVE_ONLY(t) \
static_assert(FEA_NOT_COPY_CONSTRUCTIBLE(t)() \
&& FEA_MOVE_CONSTRUCTIBLE(t)() \
&& FEA_NOT_COPY_ASSIGNABLE(t)() \
&& FEA_MOVE_ASSIGNABLE(t)(), \
#t " : doesn't fulfill move only")