9

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?
Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • There's an option in GCC and Clang to stop after one error, but it doesn't work too well from my experiences, as some errors have vital information left out. Of course I don't think that's quite what you're after, either :p – chris Dec 02 '13 at 18:21
  • 1
    That option is -Wfatal-errors, and is exactly what I would recommend (except if @chris's comment is right and this excludes important information). I think the compilers consider it the responsibility of the development environment (possibly IDE) of showing the errors in a presentable way. –  Dec 02 '13 at 18:24
  • @hvd, Clang's actually worked great when I was fishing for an example. I've only used it with GCC before. Anyway, as one example, look how GCC cuts off the list of candidates: http://coliru.stacked-crooked.com/a/4fdcf6e8bd878ffb – chris Dec 02 '13 at 18:30
  • I'm not sure I see the problem you're trying to solve. You assert that hundreds of compiler errors is "scary," and it's implied that this is a bad thing. Yet I disagree -- a good (experienced) C++ programmer won't be intimidated by scrolling compiler errors as they will know that often it's only the first one or few that really matter. – John Dibling Dec 02 '13 at 18:33
  • @chris And looking through the GCC bug database, that isn't considered a bug, because -Wfatal-errors is intended to be used only by automated scripts that merely care whether there's *any* error. –  Dec 02 '13 at 18:34
  • @JohnDibling: When there are *thousands* of line, it is very difficult to see the first one, because it is difficult to scroll the terminal corresponding to the first error. – Nawaz Dec 02 '13 at 18:35
  • You can reduce the noise by duplicating the static assertions in `f` in `f_impl`'s declaration (`enable_if`), but I would think the maintenance cost of that is high enough not to be worth it. –  Dec 02 '13 at 18:37
  • @Nawaz: I see. Yes that would be a problem, but isn't that more a problem of tool choice than anything else? I mean, I build from within Vim and can display that first compiler error using QuickFix with simply `:cc`. Problem solved? Or perhaps something along the lines of `g++ [...] | tee myErrors.txt`? – John Dibling Dec 02 '13 at 18:40
  • 1
    @JohnDibling `g++ [...] 2>&1 | [...]`: error messages show up on stderr, not stdout :) –  Dec 02 '13 at 18:43
  • @JohnDibling: *"I build from within Vim"* I don't know that. How do you do that? *"and can display that first compiler error using QuickFix with simply :cc"*. Interesting. Again, I don't know that either. :| – Nawaz Dec 02 '13 at 18:45
  • The command to build within vim is `:mak`. You have to configure `makeprg` for your own environment, but it's cake. QuickFix is built-in to Vim (86% sure on that one). Here's something that will get you started: http://stackoverflow.com/questions/729249/how-to-efficiently-make-with-vim – John Dibling Dec 02 '13 at 18:49
  • @hvd: Thanks. I was using "something along the lines of" in my comment as a placeholder for actual effort. :) – John Dibling Dec 02 '13 at 18:51
  • I actually prefer to get the three error messages one after another. In your particular example, the three errors are **different** and can be captured in a single compiler run, why for a recompile in a later pass? [Note this is different from: `static_assert(T(),"default constructor"); T t = T();` where there are potentially two errors reported that are really the same thing] – David Rodríguez - dribeas Dec 02 '13 at 19:26

2 Answers2

4

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)
David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
4

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.

Cassio Neri
  • 19,583
  • 7
  • 46
  • 68
  • Ohh I saw that [happening](http://coliru.stacked-crooked.com/a/a6187627a70527b6). GCC indeed skips instantiating `f_impl()` and yet still continues compiling the current function `f()` gives error on `T t = 10;`. Now it starts making sense to me. – Nawaz Dec 03 '13 at 06:39