Why is noexcept a problem for boost spirit semantic action in c++17?

Viewed 120

following code gives me weird compiler error on VS2019 with c++17 mode:

#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>

class Duration
{
  std::chrono::duration<int64_t, std::nano> d;

public:
  void addHours(uint64_t val) noexcept
  {
  }
};


int main()
{
  Duration d;
  namespace spirit = boost::spirit;
  namespace phoenix = boost::phoenix;
  namespace qi = boost::spirit::qi;
  qi::int_parser<int64_t, 10> int64_;
  std::string s = "2h";
  boost::spirit::qi::rule<std::string::iterator, Duration()> durationRule = (int64_ >> "h")[phoenix::bind(&Duration::addHours, qi::_val, qi::_1)];

  boost::spirit::qi::phrase_parse(s.begin(), s.end(), durationRule, boost::spirit::ascii::space, d);
}

If I compile it with c++14 mode all is fine, but with c++17 or c++20 it fails. I found that the problem is the noexcept keyword behind the addHours method. Can anybody explain why this is a problem?

1 Answers

The Phoenix library predates noexcept.

Since c++17 The noexcept-specification is a part of the function type and may appear as part of any function declarator.

The implication is that you run into limitations of Phoenix when using the noexcept specifications in c++17 mode.

Specifically, the thing that appears to break is return type deduction: Functions differing only in their exception specification cannot be overloaded (just like the return type, exception specification is part of function type, but not part of the function signature) (since C++17).

Improvements

In your case, since you're using C++17, why not simplify the whole thing? Live On Coliru

qi::rule<std::string::const_iterator, Duration()> durationRule //
    = qi::eps[_val = 0s]                                       //
    >> (qi::int_ >> "h")[_val += _1 * 1h];

The output

"2m" -> failed
"3h" -> 10800
"4s" -> failed

gives away my intent!

Bonus Extension 1: units

Using a simple table like

qi::symbols<char, Duration> unit_;
unit_.add
    ("ns", 1ns) ("nano", 1ns)
    ("us", 1us) ("μs", 1us) ("micro", 1us)
    ("ms", 1ms) ("milli", 1ms)
    ("s", 1s)
    ("m", 1min) ("min", 1min)
    ("h", 1h) ("hour", 1h)
    ("d", 24h) ("day", 24h)
    ;

Now with the trivial change of

    >> +(qi::int_ >> unit_)[_val += _1 * _2];

It does evaluates a boatload of interesting sequences: Live On Coliru

#include <boost/spirit/include/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
#include <chrono>
#include <iomanip>

using namespace std::chrono_literals;
using Duration = std::chrono::duration<int64_t, std::nano>;
namespace qi   = boost::spirit::qi;

int main()
{
    using namespace qi::labels; // _val, _1 etc

    qi::symbols<char, Duration> unit_;
    unit_.add
        ("ns", 1ns) ("nano", 1ns)
        ("us", 1us) ("μs", 1us) ("micro", 1us)
        ("ms", 1ms) ("milli", 1ms)
        ("s", 1s)
        ("m", 1min) ("min", 1min)
        ("h", 1h) ("hour", 1h)
        ("d", 24h) ("day", 24h)
        ;

    qi::int_parser<int64_t, 10> int64_;
    qi::rule<std::string::const_iterator, Duration(), qi::blank_type>
        durationRule         //
        = qi::eps[_val = 0s] //
        >> +(int64_ >> unit_)[_val += _1 * _2];

    for (std::string const s :
         {
             "2m",
             "3h",
             "4s",
             "1ms -1ns",
             "1 day -23h",
             "3600000000ns",
             "1 day -23h -50m +3600000000ns",
         }) //
    {
        std::cout << std::quoted(s);

        Duration d{};
        if (phrase_parse(                //
                s.begin(), s.end(),      //
                durationRule >> qi::eoi, //
                qi::blank, d))
            std::cout << " -> " << d / 1.0s << "s\n";
        else
            std::cout << " -> failed\n";
    }
}

Prints

"2m" -> 120s
"3h" -> 10800s
"4s" -> 4s
"1ms -1ns" -> 0.000999999s
"1 day -23h" -> 3600s
"3600000000ns" -> 3.6s
"1 day -23h -50m +3600000000ns" -> 603.6s

Note I've shifted some details around to make it work. Notably qi::eoi to check the entire input is consumed, and declaring the skipper in the rule (blank instead of space to exclude newlines). Without that, the rule was implicitly lexeme anyways (Boost spirit skipper issues)

Related