Section 8.3.  Intentional Diagnostic Generation
Team LiB
Previous Section Next Section

8.3. Intentional Diagnostic Generation

Why would anyone want to generate a diagnostic on purpose? After spending most of this chapter picking through a morass of template error messages, it's tempting to wish them away. Compiler diagnostics have their place, though, even once our templates are in the hands of users who may be less well equipped to decipher them. Ultimately, it all comes down to one simple idea:

Guideline

Report every error at your first opportunity.


Even the ugliest compile-time error is better than silent misbehavior, a crash, or an assertion at runtime. Moreover, if there's going to be a compiler diagnostic anyway, it's always better to issue the message as soon as possible. The reason template error messages often provide no clue to the nature and location of an actual programming problem is that they occur far too late, when instantiation has reached deep into the implementation details of a library. Because the compiler itself has no knowledge of the library's domain, it is unable to detect usage errors at the library interface boundary and report them in terms of the library's abstractions. For example, we might try to compile:



   #include <algorithm>


   #include <list>





   int main()


   {


       std::list<int> x;


       std::sort(x.begin(), x.end());


   }



Ideally, we'd like the compiler to report the problem at the point of the actual programming error, and to tell us something about the abstractions involvediterators in this case:



   main.cpp(7) : std::sort requires random access iterators, but


   std::list<int>::iterator is only a bidirectional iterator



VC++ 7.1, however, reports:



   C:\Program Files \Microsoft Visual Studio .NET 2003\Vc7 \include\


   algorithm(1795) : error C2784:


   'reverse_iterator<_RanIt>::difference_type std::operator -(const


   std::reverse_iterator<_RanIt> &,const


   std::reverse_iterator<_RanIt>  &)' : could not deduce template


   argument for 'const std::reverse_iterator<_RanIt> &' from


   'std::list<_Ty>::iterator'


           with


           [


              _Ty=int


           ]


   continued...



Notice that the error is reported inside some operator- implementation in the standard library's <algorithm> header, instead of in main() where the mistake actually is. The cause of the problem is obscured by the appearance of std::reverse_iterator, which has no obvious relationship to the code we wrote. Even the use of operator-, which hints at the need for random access iterators, isn't directly related to what the programmer was trying to do. If the mismatch between std::list<int>::iterator and std::sort's requirement of random access had been detected earlier (ideally at the point std::sort was invoked), it would have been possible for the compiler to report the problem directly.

It's important to understand that blame for the poor error message above does not lie with the compiler. In fact, it's due to a limitation of the C++ language: While the signatures of ordinary functions clearly state the type requirements on their arguments, the same can't be said of generic functions.[2] The library authors, on the other hand, could have done a few things to limit the damage. In this section we're going to cover a few techniques we can use in our own libraries to generate diagnostics earlier and with more control over the message contents.

[2] Several members of the C++ committee are currently working hard to overcome that limitation by making it possible to express concepts in C++ as first-class citizens of the type system. In the meantime, library solutions [SL00] will have to suffice.

8.3.1. Static Assertions

You've already seen one way to generate an error when your code is being detectably misused



   BOOST_STATIC_ASSERT(integral-constant-expression);



If the expression is false (or zero), a compiler error is issued. Assertions are best used as a kind of "sanity check" to make sure that the assumptions under which code was written actually hold. Let's use a classic factorial metafunction as an example:



   #include <boost/mpl/int.hpp>


   #include <boost/mpl/multiplies.hpp>


   #include <boost/mpl/equal.hpp>


   #include <boost/mpl/eval_if.hpp>


   #include <boost/mpl/prior.hpp>


   #include <boost/static_assert.hpp>





   namespace mpl = boost::mpl;





   template <class N>


   struct factorial


     : mpl::eval_if<


           mpl::equal_to<N,mpl::int_<0> >   // check N == 0


         , mpl::int_<1>                     // 0! == 1


         , mpl::multiplies<                 // N! == N * (N-1)!


               N


             , factorial<typename mpl::prior<N>::type>


           >


        >


   {


        BOOST_STATIC_ASSERT(N::value >= 0); // for nonnegative N


   };



Computing N! only makes sense when N is nonnegative, and factorial was written under the assumption that its argument meets that constraint. The assertion is used to check that assumption, and if we violate it:



   int const fact = factorial<mpl::int_<-6> >::value;



we'll get this diagnostic from Intel C++ 8.1:



   foo.cpp(22): error: incomplete type is not allowed


         BOOST_STATIC_ASSERT(N::value >= 0);


         ^


             detected during instantiation of class "factorial<N>


         [with N=mpl_::int_<-6>]" at line 25



Note that when the condition is violated, we get an error message that refers to the line of source containing the assertion.

The implementation of BOOST_STATIC_ASSERT is selected by the library based on the quirks of whatever compiler you're using to ensure that the macro can be used reliably at class, function, or namespace scope, and that the diagnostic will always refer to the line where the assertion was triggered. When the assertion fails on Intel C++, it generates a diagnostic by misusing an incomplete typethus the message "incomplete type is not allowed"though you can expect to see different kinds of errors generated on other compilers.

8.3.2. The MPL Static Assertions

The contents of the diagnostic above could hardly be more informative: not only is the source line displayed, but we can see the condition in question and the argument to factorial. In general, though, you can't rely on such helpful results from BOOST_STATIC_ASSERT. In this case we got them more by lucky accident than by design.

  1. If the value being tested in the assertion (-6) weren't present in the type of the enclosing template, it wouldn't have been displayed.

  2. This compiler only displays one source line at the point of an error; had the macro invocation crossed multiple lines, the condition being tested would be at least partially hidden.

  3. Many compilers don't show any source lines in an error message. GCC 3.3.1, for example, reports:



     foo.cpp: In instantiation of 'factorial<mpl_::int_<-6> >':


     foo.cpp:25:   instantiated from here


     foo.cpp:22: error: invalid application of 'sizeof' to an


     incomplete type



Here, the failed condition is missing.

The MPL supplies a suite of static assertion macros that are actually designed to generate useful error messages. In this section we'll explore each of them by using it in our factorial metafunction.

8.3.2.1 The Basics

The most straightforward of these assertions is used as follows:



   BOOST_MPL_ASSERT((bool-valued-nullary-metafunction))



Note that double parentheses are required even if no commas appear in the condition.

Here are the changes we might make to apply this macro in our factorial example:



   ...


   #include <boost/mpl/greater_equal.hpp>


   #include <boost/mpl/assert.hpp>





   template <class N>


   struct factorial


     ...


   {


       BOOST_MPL_ASSERT((mpl::greater_equal<N,mpl::int_<0> >));


   };



The advantage of BOOST_MPL_ASSERT is that it puts the name of its argument metafunction in the diagnostic. GCC now reports:



   foo.cpp: In instantiation of 'factorial<mpl_::int_<-6> >':


   foo.cpp:26:   instantiated from here


   foo.cpp:23: error: conversion from '


      mpl_::failed**********boost::mpl::greater_equal<mpl_::int_<-6>,


      mpl_::int_<0> >::***********' to non-scalar type '


      mpl_::assert<false>' requested


   foo.cpp:23: error: enumerator value for '


      mpl_assertion_in_line_23' not integer constant



Note that the violated condition is now displayed prominently, bracketed by sequences of asterisks, a feature you can count on across all supported compilers.

8.3.2.2 A More Likely Assertion

In truth, the diagnostic above still contains a great many characters we don't care about, but that's due more to the verbosity of using templates to express the failed condition -6 >= 0 than to anything else. BOOST_MPL_ASSERT is actually better suited to checking other sorts of conditions. For example, we might try to enforce N's conformance to the integral constant wrapper protocol as follows:



   BOOST_MPL_ASSERT((boost::is_integral<typename N::value_type>));



To trigger this assertion, we could write:



   // attempt to make a "floating point constant wrapper"


   struct five : mpl::int_<5> { typedef double value_type; };


   int const fact = factorial<five>::value;



yielding the following diagnostic, with a much better signal-to-noise ratio than our nonnegative test:



   ...


   foo.cpp:24: error: conversion from


   'mpl_::failed************boost::is_integral<double>::************'


   to non-scalar type 'mpl_::assert<false>' requested


   ...



8.3.2.3 Negative Assertions

Negating a condition tested with BOOST_STATIC_ASSERT is as simple as preceding it with !, but to do the same thing with BOOST_MPL_ASSERT we'd need to wrap the predicate in mpl:: not_<...>. To simplify negative assertions, MPL provides BOOST_MPL_ASSERT_NOT, which does the wrapping for us. The following rephrases our earlier assertion that N is nonnegative:



   BOOST_MPL_ASSERT_NOT((mpl::less<N,mpl::int_<0> >));



As you can see, the resulting error message includes the mpl::not_<...> wrapper:



   foo.cpp:24: error: conversion from 'mpl_::failed


   ************boost::mpl::not_<boost::mpl::less<mpl_::int_<-5>,


   mpl_::int_<0> > >::************' to non-scalar type


   'mpl_::assert<false>' requested



8.3.2.4 Asserting Numerical Relationships

We suggested that BOOST_MPL_ASSERT was not very well suited for checking numerical conditions because not only the diagnostics, but the assertions themselves tend to incur a great deal of syntactic overhead. Writing mpl::greater_equal<x,y> in order to say x >= y is admittedly a bit roundabout. For this sort of numerical comparison, MPL provides a specialized macro:



   BOOST_MPL_ASSERT_RELATION(


     integral-constant, comparison-operator, integral-constant);



To apply it in our factorial metafunction, we simply write:



   BOOST_MPL_ASSERT_RELATION(N::value, >=, 0);



In this case, the content of generated error messages varies slightly across compilers. GCC reports:



   ...


   foo.cpp:30: error: conversion from


   'mpl_::failed************mpl_::assert_relation<greater_equal, -5,


   0>::************' to non-scalar type 'mpl_::assert<false>'


   requested


   ...



while Intel says:



   foo.cpp(30): error: no instance of function template


   "mpl_::assertion_failed" matches the argument list





     argument types are: (mpl_::failed


     ************mpl_::assert_relation<  mpl_::operator>=, -5L, 0L


     >::************)





         BOOST_MPL_ASSERT_RELATION(N::value, >=, 0);


         ^


             detected during instantiation of class "factorial<N>


         [with N=mpl_::int_<-5>]" at line 33



These differences notwithstanding, the violated relation and the two integral constants concerned are clearly visible in both diagnostics.

8.3.2.5 Customized Assertion Messages

The assertion macros we've seen so far are great for a library's internal sanity checks, but they don't always generate messages in the most appropriate form for library users. The factorial metafunction probably doesn't illustrate that fact very well, because the predicate that triggers the error (N < 0) is such a straightforward function of the input. The prerequisite for computing N! is that N be nonnegative, and any user is likely to recognize a complaint that N >= 0 failed as a direct expression of that constraint.

Not all static assertions have that property, though: often an assertion reflects low-level details of the library implementation, rather than the abstractions that the user is dealing with. One example is found in the dimensional analysis code from Chapter 3, rewritten here with BOOST_MPL_ASSERT:



   template <class OtherDimensions>


   quantity(quantity<T,OtherDimensions> const& rhs)


     : m_value(rhs.value())


   {


       BOOST_MPL_ASSERT((mpl::equal<Dimensions,OtherDimensions>));


   }



What we'll see in the diagnostic, if this assertion fails, is that there's an inequality between two sequences containing integral constant wrappers. That, combined with the source line, begins to hint at the actual problem, but it's not very to-the-point. The first thing a user needs to know when this assertion fails is that there's a dimensional mismatch. Next, it would probably be helpful to know the identity of the first fundamental dimension that failed to match and the values of the exponents concerned. None of that information is immediately apparent from the diagnostic that's actually generated, though.

With a little more control over the diagnostic, we could generate messages that are more appropriate for users. We'll leave the specific problem of generating errors for dimensional analysis as an exercise, and return to the factorial problem to explore a few techniques.

Customizing the Predicate

To display a customized message, we can take advantage of the fact that BOOST_MPL_ASSERT places the name of its predicate into the diagnostic output. Just by writing an appropriately named predicate, we can make the compiler say anything we likeas long as it can be expressed as the name of a class. For example:



   // specializations are nullary metafunctions that compute n>0


   template <int n>


   struct FACTORIAL_of_NEGATIVE_NUMBER


     : mpl::greater_equal<mpl::int_<n>, mpl::int_<0> >


   {};


   template <class N>


   struct factorial


     : mpl::eval_if<


           mpl::equal_to<N,mpl::int_<0> >


         , mpl::int_<1>


         , mpl::multiplies<


               N


             , factorial<typename mpl::prior<N>::type>


           >


       >


   {


       BOOST_MPL_ASSERT((FACTORIAL_of_NEGATIVE_NUMBER<N::value>));


   };



Now GCC reports:



   foo.cpp:30: error: conversion from 'mpl_::failed


   ************FACTORIAL_of_NEGATIVE_NUMBER<-5>::************' to


   non-scalar type 'mpl_::assert<false>' requested



One minor problem with this approach is that it requires interrupting the flow of our code to write a predicate at namespace scope, just for the purpose of displaying an error message. This strategy has a more serious downside, though: The code now appears to be asserting that N::value is negative, when in fact it does just the opposite. That's not only likely to confuse the code's maintainers, but also its users. Don't forget that some compilers (Intel C++ in this case) will display the line containing the assertion:



   foo.cpp(30): error: no instance of function template


   "mpl_::assertion_failed" matches the argument list





        argument types are: (mpl_::failed


        ************FACTORIAL_of_NEGATIVE_NUMBER<-5>::************)





         BOOST_MPL_ASSERT((FACTORIAL_of_NEGATIVE_NUMBER<N::value>));


         ^



If we choose the message text more carefully, we can eliminate this potential source of confusion:



    template <int n>


    struct FACTORIAL_requires_NONNEGATIVE_argument


      : mpl::greater_equal<mpl::int_<n>, mpl::int_<0> >


    {};


    ...


        BOOST_MPL_ASSERT((


            FACTORIAL_requires_NONNEGATIVE_argument<N::value>));



Those kinds of linguistic contortions, however, can get a bit unwieldy and may not always be possible.

Inline Message Generation

MPL provides a macro for generating custom messages that doesn't depend on a separately written predicate class, and therefore doesn't demand quite as much attention to exact phrasing. The usage is as follows:



    BOOST_MPL_ASSERT_MSG(condition, message, types);



where condition is an integral constant expression, message is a legal C++ identifier, and types is a legal function parameter list. For example, to apply BOOST_MPL_ASSERT_MSG to factorial, we could write:



    BOOST_MPL_ASSERT_MSG(


        N::value >= 0, FACTORIAL_of_NEGATIVE_NUMBER, (N));



yielding this message from GCC:



    foo.cpp:31: error: conversion from 'mpl_::failed


    ****************(factorial<mpl_::int_<-5>


    >::FACTORIAL_of_NEGATIVE_NUMBER::****************)


    (mpl_::int_<-5>)' to non-scalar type 'mpl_::assert<false>'


    requested.



We've highlighted the message and the types arguments where they appear in the diagnostic above. In this case, types isn't very interesting, since it just repeats mpl_::int_<-5>, which appears elsewhere in the message. We could therefore replace (N) in the assertion with the empty function parameter list, (), to get:



    foo.cpp:31: error: conversion from 'mpl_::failed


    ****************(factorial<mpl_::int_<-5>


    >::FACTORIAL_of_NEGATIVE_NUMBER::****************)


    ()' to non-scalar type 'mpl_::assert<false>'


    requested.



In general, even using BOOST_MPL_ASSERT_MSG requires some care, because the types argument is used as a function parameter list, and some types we might like to display have special meaning in that context. For example, a void parameter will be omitted from most diagnostics, since int f(void) is the same as int f(). Furthermore, void can only be used once: int f(void, void) is illegal syntax. Also, array and function types are interpreted as pointer and function pointer types respectively:



    int f(int x[2], char* (long))



is the same as



    int f(int *x, char* (*)(long))



In case you don't know enough about the types ahead of time to be sure that they'll be displayed correctly, you can use the following form, with up to four types:



    BOOST_MPL_ASSERT_MSG(condition, message, (types<types >));



For example, we could add the following assertion to factorial, based on the fact that all integral constant wrappers are classes:



    BOOST_MPL_ASSERT_MSG(


        boost::is_class<N>::value


      , NOT_an_INTEGRAL_CONSTANT_WRAPPER


      , (types<N>));



If we then attempt to instantiate factorial<void>, VC++ 7.1 reports:



    foo.cpp(34) : error C2664: 'mpl_::assertion_failed' : cannot


            convert parameter 1 from 'mpl_::failed


            ****************(__thiscall


            factorial<N>::NOT_an_INTEGRAL_CONSTANT_WRAPPER::*


            ***************               )(mpl_::assert_::types<T1>)


            ' to 'mpl_::assert<false>::type'


            with


            [


                N=void,


                T1=void


            ]



Since types can accept up to four arguments, the diagnostic is a little better here than on compilers that don't elide default template arguments. For example, the diagnostic from Intel C++ 8.0 is:



    foo.cpp(31): error: no instance of function template


    "mpl_::assertion_failed" matches the argument list





         argument types are: (mpl_::failed ****************


         (factorial<void>::NOT_an_INTEGRAL_CONSTANT_WRAPPER::


         ****************)(mpl_::assert_::types<void, mpl_::na,


         mpl_::na, mpl_::na>))





          BOOST_MPL_ASSERT_MSG(


          ^


              detected during instantiation of class "factorial<N>


              [with N=void]" at line 37



It's also worth noticing that, while the customized predicate we wrote for use with BOOST_MPL_ASSERT was written at namespace scope, the message generated by BOOST_MPL_ASSERT_MSG appears as a qualified member of the scope where the assertion was issued (factorial<void> in this case). As a result, compilers that do deep typedef substitution have one more opportunity to insert unreadable type expansions in the diagnostic. For example, if we instantiate:



    mpl::transform<mpl::vector<void>, factorial<mpl::_> >



Intel C++ 8.0 generates the following:



    foo.cpp(34): error: no instance of function template


    "mpl_::assertion_failed" matches the argument list





        argument types are: (mpl_::failed


        ****************(factorial<boost::mpl::bind1<


        boost::mpl::quote1<factorial, boost::mpl::void_>,


        boost::mpl::lambda<mpl_::_,


        boost::mpl::void_>::result_>::apply<


        boost::mpl::bind1<factorial<mpl_::_>,


        mpl_::_2>::apply<boost::mpl::aux::fold_impl<1,


        boost::mpl::begin<boost::mpl::vector<void, mpl_::na,


        mpl_::na, mpl_::na, mpl_::na, mpl_::na, mpl_::na, mpl_::na,


        mpl_::na, mpl_::na, mpl_::na, mpl_::na, mpl_::na, mpl_::na,


        mpl_::na, mpl_::na, mpl_::na,


        ...four similar long lines omitted...





          mpl_::na>::t1>::NOT_an_INTEGRAL_CONSTANT_WRAPPER::


          ****************)(mpl_::assert_::types<boost::mpl::bind1<


          line continued...





          ...five similar lines omitted...


        BOOST_MPL_ASSERT_MSG(


        ^



The omission of nine long lines above actually contributes a great deal to the message's readability, so you can probably imagine what it's like to read the whole thing.

Selecting a Strategy

Both approaches to customized error generation we've covered here have strengths and weaknesses: BOOST_MPL_ASSERT_MSG is convenient, minimal, and highly expressive of its intent, but it can require some care if asked to display void, array, and function types, and it can have readability problems, especially in the presence of deep typedef substitution. Using custom predicates with BOOST_MPL_ASSERT offers a little more control over message formatting, though it takes more work, complicates code somewhat, and can be confusing unless the predicate name is carefully chosen. Clearly there's no perfect strategy for all needs, so consider the trade-offs carefully before selecting one.

8.3.3. Type Printing

When a template metaprogram misbehaves, it can begin to seem like an impenetrable black box, especially if the problem doesn't manifest itself in a compilation error, or if the error shows up long after the actual problem has occurred. Sometimes it's useful to intentionally generate a diagnostic just, well, for diagnostic purposes. For most situations, this simple tool suffices:



    template <class T> struct incomplete;



If at any point we need to know what some type T is, all we have to do is to cause incomplete<T> to be instantiated, for example:



    template <class T>


    struct my_metafunction


    {


        incomplete<T> x; // temporary diagnostic


        typedef ... type;


     };



Most C++ compilers, indeed, all the compilers we've seen, will generate an error message that shows us what T is.[3] This technique is subject to the usual caveats: Compilers that do deep typedef substitution may show us an arbitrarily complicated name for T, depending on how T was computed.

[3] Note that we did not write typedef incomplete<T> x; because that would not cause incomplete<T> to be instantiated, as described in Chapter 2.

One time-honored technique for debugging C/C++ programs is to "stick printfs in the code" and examine the resulting execution log. The incomplete<T> technique is more analagous to a runtime assertion, though: It shows us the program state in question and causes a hard error. Remember when we said that most C++ compilers don't recover well from errors? Even if your compiler forges ahead after instantiating incomplete<T>, the results are about as reliable as what you'd expect from a program that had reported runtime data corruption.

To generate a compile-time execution log, we'd need a way to generate a non-error diagnostic messagea warning. Because there's no single construct that will cause all compilers to generate a warning (indeed, most compilers let you disable warnings altogether), MPL has a print metafunction that is just like identity except that it is tuned to generate a warning on a variety of popular compilers with their "usual settings." For example, the following program:



    template <class T, class U>


    struct plus_dbg


    {


        typedef typename


          mpl::print< typename mpl::plus<T,U>::type >::type


        type;


    };





    typedef mpl::fold<


        mpl::range_c<int,1,6>


      , mpl::int_<0>


      , plus_dbg<_1,_2>


    >::type sum;



produces the following diagnostics (among others) with GCC:[4]

[4] One peculiar quirk of GCC is that the use of metafunction forwarding interferes slightly with diagnostics. Had we instead written:









          template <class T, class U>


          struct plus_dbg


            : mpl::print< typename mpl::plus<T,U>::type >


          {};





The diagnostics beginning with "In instantiation of..." would have had a filename label somewhere in MPL's implementation headers instead of in foo.cpp. While this problem is not enough to prevent us from recommending metafunction forwarding with GCC, it is worth being aware of.



    foo.cpp: In instantiation of


    'boost::mpl::print<boost::mpl::integral_c<int, 1> >':


    ...


    foo.cpp:72: warning: comparison between signed and unsigned


    integer expressions





    foo.cpp: In instantiation of


    'boost::mpl::print<boost::mpl::integral_c<int, 3> >':


    ...


    foo.cpp:72: warning: comparison between signed and unsigned


    integer expressions





    foo.cpp: In instantiation of


    'boost::mpl::print<boost::mpl::integral_c<int, 6> >':


    ...


    foo.cpp:72: warning: comparison between signed and unsigned


    integer expressions





    foo.cpp: In instantiation of


    'boost::mpl::print<boost::mpl::integral_c<int, 10> >':


    ...


    foo.cpp:72: warning: comparison between signed and unsigned


    integer expressions





    foo.cpp: In instantiation of


    'boost::mpl::print<boost::mpl::integral_c<int, 15> >':


    ...


    foo.cpp:72: warning: comparison between signed and unsigned


    integer expressions



Naturally, these messages are mixed into the compiler's instantiation backtraces. This is another area where diagnostic filtering tools can help: STLFilt has an option (/showback:N) that eliminates the backtrace material shown as the ellipsis (...) above, so that we're left with a simplified trace of compile time execution. Of course, if you have access to UNIX tools, piping the errors into "grep print" might do the job just as easily.

    Team LiB
    Previous Section Next Section