Improving diagnostics with the help of static_assert

1.5k views Asked by At

In template programming, static_assert helps programmers to check constraint(s) on template arguments and generate human readable error messages on violation of constraint(s).

Consider this code,

template<typename T>
void f(T)
{
    static_assert(T(), "first requirement failed to meet.");

    static_assert(T::value, "second requirement failed to meet.");    

    T t = 10; //even this may generate error!
}

My thought is : if the first static_assert fails, it means some requirement on T doesn't meet, hence the compilation should stop, generating only the first error message — because it doesn't make much sense to continue the compilation just to generate more and more error messages, most of which often point to a single constraint violation. Hundreds of error messages, instead of just one, look very scary on the screen — I would even say, it defies the very purpose of static_assert to some extent.

For example, if I call the above function template as:

f(std::false_type{});

GCC 4.8 generates the following:

main.cpp: In instantiation of 'void f(T) [with T = std::integral_constant<bool, false>]':
main.cpp:16:24:   required from here
main.cpp:7:5: error: static assertion failed: first requirement failed to meet.
     static_assert(T(), "first requirement failed to meet.");
     ^
main.cpp:9:5: error: static assertion failed: second requirement failed to meet.
     static_assert(T::value, "second requirement failed to meet.");    
     ^
main.cpp:11:11: error: conversion from 'int' to non-scalar type 'std::integral_constant<bool, false>' requested
     T t = 10; //even this may generate error!

As you can see (online), that is too much of error. If the first static_assert fails, it is very much likely that the rest of the code will also fail if compilation continues, then why continue compilation? In template programming, I'm sure many programmers don't want such cascading error messages!

I tried to solve this issue by splitting the function into multiple functions, in each checking only one constraint, as:

template<typename T>
void f_impl(T); //forward declaration

template<typename T>
void f(T)
{
    static_assert(T(), "first requirement failed to meet.");
    f_impl(T());
}

template<typename T>
void f_impl(T)
{
    static_assert(T::value, "second requirement failed to meet.");     
    T t = 10;
}  

f(std::false_type{}); //call

Now this generates this:

main.cpp: In instantiation of 'void f(T) [with T = std::integral_constant<bool, false>]':
main.cpp:24:24:   required from here
main.cpp:10:5: error: static assertion failed: first requirement failed to meet.
     static_assert(T(), "first requirement failed to meet.");
     ^

That is a lot of improvement — just one error message is a lot easier to read and understand (see online).

My question is,

  • Why does the compilation not stop on the first static_assert?
  • Since splitting of function template and checking one constraint in each function_impl, helps only GCC and clang still generates lots of error, is there any way to improve diagnostics in a more consistent way — something which works for all compilers?
2

There are 2 answers

1
Cassio Neri On BEST ANSWER

I agree with David Rodríguez - dribeas and in defense of compiler writers consider this example:

#include <type_traits>

class A {};

// I want the nice error message below in several functions.
// Instead of repeating myself, let's put it in a function.
template <typename U>
void check() {
    static_assert(std::is_convertible<U*, const volatile A*>::value,
        "U doesn't derive publicly from A "
        "(did you forget to include it's header file?)");
}

template <typename U>
void f(U* u) {
    // check legality (with a nice error message)
    check<U>();
    // before trying a failing initialization:
    A* p = u;
}

class B; // I forget to include "B.h"

int main() {
    B* b = nullptr;
    f(b);
}

When the instantiation of f<B> starts the compiler (or the compiler writter) might think: "Humm... I need to instantiate check<U> and people always complain that compiling templates is too slow. So I'll keep going and perhaps there's something wrong below and I don't event need to instantiate check."

I believe the reasoning above makes sense. (Notice that I'm not a compiler writter so I'm just speculating here).

Both GCC 4.8 and VS2010 keep compiling f<B>, postponing the instantiation of check<B> for later. Then they find the failing initialization and provide their own error messages. VS2010 stops immediately and I don't get my nice error message! GCC keeps going and yields the message that I wanted (but only after its own).

Metaprogramming is tricky for the programmers and for the compilers. static_assert helps a lot but it's not a panacea.

0
David Rodríguez - dribeas On

There are multiple goals that need to be balanced here. In particular, smaller simpler error messages may be attained by stopping on the first error, which is good. At the same time stopping on the first error does not give you information about any other issues that you might want to solve before attempting another potentially expensive compilation. For example, in your first example I personally prefer all of the static_asserts to be checked at once. Read the error message as:

You failed to meet the following requirements:

  • default constructor
  • nested value type

I'd rather have both those errors detected in the first pass, than fix one and have to way a few minutes for the build system to trip on the next one.

The premise here is that the compiler is able recover from the error and continue parsing, although the grammar is context dependent and that is not always the case, so part of the negative side of the problem is that you can trust the first error, but the next errors might be just a consequence of that first one, and it takes experience to realize which is which.

All this is quality of implementation (thus compiler dependent), and many implementations let you determine when to stop, so it is up to the user and the flags that are passed to the compiler. Compilers are getting better reporting errors and recovering from them, so you can expect improvements here. To further improve things, > C++14 (C++17? later?) will add concepts that are intended to improve the error messages.

Summarizing:

  • This is quality of implementation and can be controlled with compiler flags
  • Not everyone wants what you want, some want to detect more than one error in each compiler pass
  • The future will come with better error messages (concepts, compiler improvements)