Search code examples
c++boost-spirit

How to round a number during QI parsing with boost?


With a simple Boost qi grammar, how can I make it rounding my number?

This is the parser:

factor =
            float_                          [_val = _1]
            |   ('-' >> factor              [_val = -_1])
            |   ('+' >> factor              [_val = _1])
            ;

This can parse a float and it can be also negative.

I want to round the float, so I would add something like this to the grammar:

|   ('~' >> factor              [_val = round(_1)])

But this results a compile-time error:

no type named ‘__type’ in ‘struct __gnu_cxx::__enable_if<false, double>’

This error is not too informative for me, can you please help? I want to be able to round a number, ie:

~1.8 -> 2
~1.2 -> 1

Note: I'm parsing with phrase_parse.


Solution

  • Semantic actions require Phoenix Actors, which are deferred functions.

    Options:

    1. Adaptation macros https://www.boost.org/doc/libs/1_73_0/libs/phoenix/doc/html/phoenix/modules/function/adapting_functions.html
    2. phoenix::function<>
    3. phoenix::bind
    4. write your own, see for more details https://www.boost.org/doc/libs/1_68_0/libs/spirit/doc/html/spirit/qi/tutorials/semantic_actions.html#spirit.qi.tutorials.semantic_actions.examples_of_semantic_actions

    Simplified Test Bed

    Just parsing a number:

    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    namespace qi = boost::spirit::qi;
    
    int main() {
        std::string s = "1.75";
    
        double v;
        if (qi::parse(begin(s), end(s), qi::double_, v)) {
            std::cout << "Parsed: " << v << "\n";
        }
    }
    

    Prints Live On Coliru:

    Parsed: 1.75
    

    Adaptation

    Using the macros:

    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <cmath>
    namespace qi = boost::spirit::qi;
    
    int main() {
        std::string s = "1.75";
    
        double v;
        if (qi::parse(begin(s), end(s), qi::double_, v)) {
            std::cout << "Parsed: " << v << "\n";
            std::cout << "Rounded: " << round(v) << "\n";
        }
    }
    

    Prints Live On Coliru:

    Parsed: 2
    

    function<>

    You can get away with hardcoding a signature here:

    boost::phoenix::function<double(*)(double)> round_(::round);
    

    However the real power comes with polymorphic calleables:

    struct round_f {
        template <typename T> auto operator()(T const& v) const {
            using std::round; // activate ADL
            return round(v);
        }
    };
    boost::phoenix::function<round_f> round_{};
    

    Now you can use the round_ actor on any type that has a free-function overload round overload that is compatible. Handy if tomorrow you decide to parse long double, float or boost::multiprecision::cpp_dec_float.

    See it Live On Coliru

    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <cmath>
    namespace qi = boost::spirit::qi;
    
    struct round_f {
        template <typename T> auto operator()(T const& v) const {
            using std::round; // activate ADL
            return round(v);
        }
    };
    boost::phoenix::function<round_f> round_{};
    
    int main() {
        std::string s = "1.75";
    
        double v;
        using namespace qi::labels;
        if (qi::parse(begin(s), end(s), qi::double_ [ _val = round_(_1) ], v)) {
            std::cout << "Parsed: " << v << "\n";
        }
    }
    

    Prints

    Parsed: 2
    

    Using phoenix::bind

    As a lower-level building block, you can bind unwrapped calleables:

    if (qi::parse(begin(s), end(s), qi::double_ 
                [ _val = phoenix::bind(round_f{}, _1) ], v))
    {
        std::cout << "Parsed: " << v << "\n";
    }
    

    If you don't mind ugly:

    if (qi::parse(begin(s), end(s), qi::double_ 
        [ _val = phoenix::bind(static_cast<double(&)(double)>(std::round), _1) ], v))
    {
        std::cout << "Parsed: " << v << "\n";
    }
    

    See both Live On Coliru