Boost logo

Boost :

Subject: Re: [boost] Unittest capability for meta-programs feedback request
From: Ábel Sinkovics (abel_at_[hidden])
Date: 2011-09-27 14:24:21


Hi Ben,

> This submission would make it possible to write a complete set of unit
> tests for meta-programs, to test both the positive, compiling statements,
> and the negative, non-compiling statements. These tests will all compile,
> and the negative tests can throw an exception instead of failing to compile.

By negative tests do you mean compile-time predicates that return false?

> The submission source is available at https://github.com/icaretaker/Metatest.
> The provided examples are based on the factorial metafunction from Chapter
> 8 of "C++ Template Metaprogramming" by David Abrahams and Aleksey Gurtovoy
> - http://www.boostpro.com/mplbook/. Chapter 8 explains the rational behind
> the BOOST_MPL_ASSERT_* macros. This submission complements these macros by
> allowing the writing of regression unit tests, to ensure the user will
> encounter the mpl formatted compiler error, if the library types are
> incorrectly instantiated.

Your factorial (example_boosttest_factorial.cpp) example uses a
compile-time assertion to check the validity of the argument of
factorial. In my understanding of what you wrote (and how your example
uses it), you intend to throw a runtime exception from the default
constructor of the factorial class.

What a metafunction such as factorial could do for handling errors is
throwing a "compile-time exception". This is something I've implemented
in an other library (metamonad) in my mpllibs repository. This is a
bunch of tools simulating exceptions at compile-time. One can "throw"
and "catch" them. The "exception" that is "thrown" can contain a class
describing the problem itself in a meaningful way. These simulated
exceptions can be propagated in the template metafunction call-chain.
The fact that an "exception" is propagated out of a metaprogram doesn't
break the compilation on its own. It generates errors when the
metaprogrammer tries to use it as it was the real result.

When the metafunction is used in a compile-time predicate and an
exception is propagated out, metatest can display it or add it to
Boost.Test (including the meaningful description of the problem) using
pretty-printing.

When a metaprogram is used in real code and an "exception" is propagated
out of it, it will most likely not be usable in the context the
metaprogrammer is trying to use it in, thus it generates a compilation
error. What I've found useful in such situations is compiling the
problematic metaprogram on its own and display it with the
pretty-printing solution of metatest.

Simulated exceptions are implemented using monads. A drawback of this is
that every metafunction in the call-chain has to be prepared for
propagating exceptions explicitly. However, metamonad provides a
template class (try_) adding this to existing metafunctions. It can be
used the following way:

template </* args */>
struct metafunction_not_prepared_for_exception_propagation :
  /* body */
{};

template </* args */>
struct metafunction_prepared_for_exception_propagation :
  metatest::try_< /* body */ >
{};

Your example:

template <class N>
struct factorial
    : mpl::eval_if<
      mpl::less_equal<N, mpl::int_<0> >
    , mpl::int_<1>
    , mpl::times<N, factorial<typename mpl::prior<N>::type> >
>::type
{
    BOOST_METATEST((mpl::greater_equal<N, mpl::int_<0> >));
};

BOOST_AUTO_TEST_CASE(failing_negative_homemade_framework) {
    bool caught = false;
    try {
        factorial<mpl::int_<-1> > factneg1;
    } catch (metatest_exception & ee) {
        caught = true;
    }
    BOOST_CHECK_EQUAL(caught, true);
}

could be implemented using metamonad's compile-time exceptions the
following way:

// class describing the problem
struct negative_factorial_argument {};

// adding pretty-printing support
MPLLIBS_DEFINE_TO_STREAM_FOR_TYPE(
  negative_factorial_argument,
  "The factorial metafunction has been called with a negative argument."
)

template <class N>
struct factorial :
  mpl::eval_if<
    typename mpl::greater_equal<N, mpl::int_<0> >::type,
    mpl::eval_if<
      mpl::less_equal<N, mpl::int_<0> >,
      mpl::int_<1>,
      mpl::times<N, factorial<typename mpl::prior<N>::type> >
>,
    metamonad::throw_<negative_factorial_argument>
>
{};

template <class NullaryMetafunction>
struct no_throw :
  metamonad::do_try<
    NullaryMetafunction,
    metamonad::do_return<mpl::true_>
>
{};
MPLLIBS_DEFINE_TO_STREAM_FOR_TEMPLATE(1, no_throw, "no_throw")

BOOST_AUTO_TEST_CASE(failing_negative_homemade_framework) {
  metatest::meta_check<
    no_throw<factorial<mpl::int_<-1> > >
>(MPLLIBS_HERE);
}

No runtime exceptions are used - meta_check passes the pretty-printed
"compile-time exception" to a runtime unit testing framework
(Boost.Test). The implementation of these template functions is simple,
and one can easily write similar functions to support other unit testing
frameworks.

> BOOST_METATEST(pred)
> BOOST_METATEST_NOT(pred)
> BOOST_METATEST_RELATION(x, rel, y)
> BOOST_METATEST_MSG(cond, msg, types)

If you use macros to do assertions and your predicates contain syntax
errors, the error messages will point to the macro call, not to the
exact location of the error. By using template functions for assertions
(meta_warn, meta_check, meta_require in my metatest implementation - see
my other mail metatest interface update...) you don't hide the real
location of the code from the compiler.

What is the benefit of having BOOST_METATEST_RELATION, METATEST_NOT
macros? For example why is using

BOOST_METATEST_NOT(pred)

better than using

BOOST_METATEST(lazy_not<pred>)

?

Regards,
  Abel


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk