I recently posted about defensive programming, and how you can create an easy to use, low friction library of high level defensive rules. These rules prevent “silent” modification of your class constructors and help enforce a class design by declaring compile-time validation rules. The idea was well received, but some astute readers correctly pointed out the rule of 5 implementation was lacking. Today, we fix that.

If you are new to the concept, you can checkout my previous post about the subject. It explains the benefits and use-cases of defensive programming and how the whole idea came about. One important aspect of the idiom is educational. By implementing rules as I will discuss in this post, you make it much more natural for beginner or intermediate programmers (heck, even veterans) to intuitively learn the dark art of C++ constructor generation.

Using the previous post’s technique, one would only check if constructors exist. However, the rule of 5 discusses constructor implementations, rather than their mere presence. It states - if you have to implement one of your destructor, constructors or assignment operators manually, you probably need to implement all of them. Using type_traits , we should be able to verify this.

New Design

One of the main issues I had while prototyping a solution revolved around static_asserts and their behavior in compile-time branches. Unfortunately, one cannot simply if constexpr a statement and apply static_asserts that always evaluate to a given value in that branch.

Furthermore, the macro composition and the lambdas I was using previously became unmanageable. So I decided to leave all the logic to a trait class. This class implements the required logic for a given rule. In the end, macros are only used for nicely formatted error messages, which was the goal in the first place anyways. Enforcing a rule is just as easy as before.

struct concepts_are_coming { bool dreaming = true; }; FULFILLS_RULE_OF_5(concepts_are_coming);

C++ Limitations

Before diving in the solution, and before you read this entire post, there is an unfortunate C++ caveat. A bug prevents “perfect” detection for destructors, copy constructors and move constructors. Indeed, cppreference notes :

In many implementations, is_nothrow_copy_constructible also checks if the destructor throws because it is effectively noexcept(T(arg)). Same applies to is_trivially_copy_constructible, which, in these implementations, also requires that the destructor is trivial: GCC bug 51452 LWG issue 2116.

What this means in practice is if a user implements a custom destructor, the copy constructor and the move constructor won’t be trivial (though implemented). This makes it impossible to know if a user is following the rule of 5 for the copy constructor and move constructor in that particular case.

A general rule of thumb is to implement other custom constructors/operators before the destructor. For some, this may be a deal breaker. It is certainly a party pooper. I personally still find the idiom useful, though hampered by this problem. Lets hope the issue gets addressed at some point.

Some compilers also have problems with copy constructors vs. move constructors. I am in the process of gathering repro cases to hopefully get this addressed. There is a simple fix for this problem, which is also a best practice. Mark your desired constructors as = default; , and that problem goes away.

Detecting Rule Of 5

Enforcing the rule of 5 happens in a 2 steps. First, we make sure all constructors exist. If not, we bail out early and assert with a specific message to the user. We state that some constructors are missing, and we state which ones. This means a user can follow instructions to fix the error easily.

If all the constructors, destructors and operators are present, we check whether they are all trivial or all non-trivial. If that isn’t the case, we print a high-level error message and we print which constructors are violating the rule.

Defensive Traits

Silencing specific asserts means our trait class has some unintuitive logic. Lets take a look at the trait class used to generate our rules.

namespace detail { template < class T > struct defensive { struct five { static constexpr bool generated_ctors() { return destructible && copy_constructible && move_constructible && copy_assignable && move_assignable; } static constexpr bool all_trivial() { return trivially_destructible && trivially_copy_constructible && trivially_move_constructible && trivially_copy_assignable && trivially_move_assignable; } static constexpr bool all_non_trivial() { return ! trivially_destructible && ! trivially_copy_constructible && ! trivially_move_constructible && ! trivially_copy_assignable && ! trivially_move_assignable; } static constexpr bool rule_pass() { // If we don't have 5 constructors, don't trigger static_assert // for rule of 5 user defined constructors. That error will be // caught by another static assert. return ! generated_ctors() || all_trivial() || all_non_trivial(); } // Always silence specific error messages if the rule is passing. static constexpr bool user_dtor_ok() { return rule_pass() || ! trivially_destructible; } static constexpr bool user_copy_ctor_ok() { return rule_pass() || ! trivially_copy_constructible; } static constexpr bool user_move_ctor_ok() { return rule_pass() || ! trivially_move_constructible; } static constexpr bool user_copy_ass_ok() { return rule_pass() || ! trivially_copy_assignable; } static constexpr bool user_move_ass_ok() { return rule_pass() || ! trivially_move_assignable; } }; // Some of these are used for other rules. static constexpr bool default_constructible = std :: is_default_constructible < T >:: value; static constexpr bool trivially_default_constructible = std :: is_trivially_default_constructible < T >:: value; static constexpr bool destructible = std :: is_destructible < T >:: value; static constexpr bool trivially_destructible = std :: is_trivially_destructible < T >:: value; static constexpr bool copy_constructible = std :: is_copy_constructible < T >:: value; static constexpr bool trivially_copy_constructible = std :: is_trivially_copy_constructible < T >:: value; static constexpr bool move_constructible = std :: is_move_constructible < T >:: value; static constexpr bool trivially_move_constructible = std :: is_trivially_move_constructible < T >:: value; static constexpr bool nothrow_move_constructible = std :: is_nothrow_move_constructible < T >:: value; static constexpr bool copy_assignable = std :: is_copy_assignable < T >:: value; static constexpr bool trivially_copy_assignable = std :: is_trivially_copy_assignable < T >:: value; static constexpr bool move_assignable = std :: is_move_assignable < T >:: value; static constexpr bool trivially_move_assignable = std :: is_trivially_move_assignable < T >:: value; };

Our top level rule checks are self-explanatory. We use the collected class traits and verify if all constructors exist, if they are all trivial or if they are all non-trivial. As mentioned previously, we shouldn’t be using branches for our static_asserts . Instead, we silence checks if a high-level rule fails. So, rule_pass will always return true if there are some missing constructors/destructor/operators. This silences our rule of 5 checks, and falls back to the other error messages.

The same applies to our individual constructor/destructor/operator checks. If generated_ctors fails, rule_pass will return true , bypassing those asserts. If all constructors are present, only then do we check for specific constructor implementations.

Macros

The high-level macros are quite simplified (yay) and are only in charge of printing nice error messages. The original rule that checks whether all constructors, destructor and operators are present is still used and available.

#define FEA_FULFILLS_5_CTORS(t) \ static_assert(detail::defensive<t>::five::generated_ctors(), \ #t " : requires destructor, copy and move constructor, copy and " \ "move assignement operator"); \ static_assert(detail::defensive<t>::destructible, \ " - " #t " : must be destructible"); \ static_assert(detail::defensive<t>::copy_constructible, \ " - " #t " : must be copy constructible"); \ static_assert(detail::defensive<t>::move_constructible, \ " - " #t " : must be move constructible"); \ static_assert(detail::defensive<t>::copy_assignable, \ " - " #t " : must be copy assignable"); \ static_assert(detail::defensive<t>::move_assignable, \ " - " #t " : must be move assignable")

There is no trickery here. All these asserts must be true for the rule to pass.

Next, we have a FULFILLS_RULE_OF_5 macro. It uses FULFILLS_5_CTORS first and later applies all our specific checks. These will only trigger if FULFILLS_5_CTORS passes, and the class isn’t all trivial or all non trivial.

#define FEA_FULFILLS_RULE_OF_5(t) \ FEA_FULFILLS_5_CTORS(t); \ static_assert(detail::defensive<t>::five::rule_pass(), \ #t " : doesn't fulfill rule of 5"); \ static_assert(detail::defensive<t>::five::user_dtor_ok(), \ " - " #t " : must implement user-defined destructor"); \ static_assert(detail::defensive<t>::five::user_copy_ctor_ok(), \ " - " #t " : must implement user-defined copy constructor"); \ static_assert(detail::defensive<t>::five::user_move_ctor_ok(), \ " - " #t " : must implement user-defined move constructor"); \ static_assert(detail::defensive<t>::five::user_copy_ass_ok(), \ " - " #t \ " : must implement user-defined copy assignement operator"); \ static_assert(detail::defensive<t>::five::user_move_ass_ok(), \ " - " #t \ " : must implement user-defined move assignement operator")

Aaaand we’re done. Our rule of 5 enforcement macro will trigger whenever possible, making sure your class defines all constructors, destructor and operators. If you implement something manually, the rule will trigger appropriate asserts with helpful messages geared toward teaching the intricacies of the C++ language.

Though it is currently imperfect, I find this whole idiom very useful and promising. I rarely write any new code without it. I’ll be keeping an eye out for concepts, as they may open up some more interesting possibilities to enforce class behaviors. Defensive programming isn’t just about constructors, destructors and operators after all.

You can find the full library on my github, along with more rules and examples. I look forward to suggestions on how to improve it. Many thanks to Philippe Sawicki for his input on the library and Adam Berckmans for being very Italian.