Thursday, August 28, 2014

Asserting compilation errors in C++

Sometimes when crafting an interface, we want to ensure that some illegal constructs leads to compilation errors. After all, a good interface is easy to use correctly, and difficult to get wrong, and what can be more difficult to get wrong than something that doesn't compile?

We also know that untested often is buggy, or at least we cannot be sure that it is correct, and tests tests that aren't automated tend to be forgotten. This, of course, means that even if you've carefully crafted an interface where some construction is illegal, and made some manual tests for it, some bug fix later might ruin it and no test will catch that the illegal construct now compiles.

However, namespaces and using directives opens an opportunity. Below is the beginnings of a trap that catches illegal calls to a function named f.
int f(std::string); // function to trap abuse of

struct illegal;
namespace bait {
  illegal f(...);

using namespace bait;

f(3);   // calls bait::f
f("");  // calls ::f
With the aid of the C++11 decltype specifier, we can catch the resulting type of an expression at compile time, in this case getting the return type of a function call, without actually making the call.
decltype(f(3))  obj1; // illegal
decltype(f("")) obj2; // int
Note that bait::f is never implemented. All that is needed is the signature so that the compiler can find match the arguments and get the return type.

With these two, the trap can be triggered at compile time using C++11 static_assert and std::is_same<T,U>
#include <type_traits>

              "compiles when it shouldn't");
The above compiles, since f(3) matches bait::f, and no code is generated. Changing to a match of ::f, however
              "compiles when it shouldn't");
gives a compilation error. On clang++ 3.4.2 the message is:
fc.cpp:18:1: error: static_assert failed "compiles when it shouldn't"
static_assert(std::is_same<decltype(f("")), illegal>::value,
^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
and g++ 4.8.2 gives the message:
fc.cpp:18:1: error: static assertion failed: compiles when it shouldn't
 static_assert(std::is_same<decltype(f("")), illegal>::value,
The results seem reversed. A call that would cause compilation error compiles, and a call that would compile gives a compilation error.

Now what if f is in a namespace? Things becomes marginally more cluttered, but the technique remains the same. Putting the bait in an inline namespace solves the problem.
namespace ns {
  int f(std::string);

struct illegal;

namespace ns {

  inline namespace bait {
    illegal f(...);
An inline namespace is a namespace, but a everything declared in it is visible in its surrounding namespace, similar to the using namespace directive used earlier.
ns::f(3);   // calls ns::bait::f
ns::f("");  // calls ns::f
The test thus becomes:
              "compiles when it shouldn't");
A macro can help with code readability:
static_assert(std::is_same<decltype(__VA_ARGS__), \
                           illegal::type>::value, \
              #__VA_ARGS__ " compiles when it shouldn't")
Writing the test code as:
gives the (g++ 4.8.2) compilation error:
fc.cpp:20:3: error: static assertion failed: ns::f("")compiles when it shouldn't
   static_assert(std::is_same<decltype(__VA_ARGS__), illegal>::value, \
fc.cpp:23:1: note: in expansion of macro ‘ASSERT_COMPILATION_ERROR’
I think this is pretty neat. It is now simple to test that illegal calls actually don't compile. You can add these tests to the unit test program that asserts the intended functionality.

No comments:

Post a Comment