2
0
mirror of https://github.com/boostorg/spirit.git synced 2026-01-19 04:42:11 +00:00

Modernize x3::with (#829)

Use concepts in `x3::with`.

`x3::with` is now a CPO that inhibits ADL.

`x3::with_directive` now holds lvalue reference by reference, and rvalue by value.
This is necessary for preventing dangling reference. However, passing dangling
lvalue reference or destroying the value bound to the reference passed to
`x3::with` technically can't be detected and it is the user's responsibility to not
do such things.

Add `x3::with` test for all 4 value categories: `T`, `T const`, `T&`, `T const&`
This commit is contained in:
Nana Sakisaka
2025-09-11 20:54:37 +09:00
committed by GitHub
parent 4cd604abf6
commit e1973fcb31
2 changed files with 286 additions and 74 deletions

View File

@@ -1,5 +1,6 @@
/*=============================================================================
Copyright (c) 2014 Joel de Guzman
Copyright (c) 2025 Nana Sakisaka
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
@@ -10,77 +11,201 @@
#include <boost/spirit/home/x3/support/unused.hpp>
#include <boost/spirit/home/x3/core/parser.hpp>
namespace boost { namespace spirit { namespace x3
#include <iterator>
#include <type_traits>
#include <utility>
namespace boost::spirit::x3
{
///////////////////////////////////////////////////////////////////////////
// with directive injects a value into the context prior to parsing.
///////////////////////////////////////////////////////////////////////////
template <typename Subject, typename Derived, typename T>
struct with_value_holder
: unary_parser<Subject, Derived>
{
typedef unary_parser<Subject, Derived> base_type;
mutable T val;
constexpr with_value_holder(Subject const& subject, T&& val)
: base_type(subject)
, val(std::forward<T>(val)) {}
};
template <typename Subject, typename Derived, typename T>
struct with_value_holder<Subject, Derived, T&>
: unary_parser<Subject, Derived>
{
typedef unary_parser<Subject, Derived> base_type;
T& val;
constexpr with_value_holder(Subject const& subject, T& val)
: base_type(subject)
, val(val) {}
};
template <typename Subject, typename ID, typename T>
struct with_directive
: with_value_holder<Subject, with_directive<Subject, ID, T>, T>
struct with_directive;
namespace detail
{
typedef with_value_holder<Subject, with_directive<Subject, ID, T>, T> base_type;
static bool const is_pass_through_unary = true;
static bool const handles_container = Subject::handles_container;
typedef Subject subject_type;
constexpr with_directive(Subject const& subject, T&& val)
: base_type(subject, std::forward<T>(val)) {}
template <typename Iterator, typename Context
, typename RContext, typename Attribute>
bool parse(Iterator& first, Iterator const& last
, Context const& context, RContext& rcontext, Attribute& attr) const
template <typename Subject, typename ID, typename T>
struct with_directive_impl
: unary_parser<Subject, with_directive<Subject, ID, T>>
{
using base_type = unary_parser<Subject, with_directive<Subject, ID, T>>;
mutable T val_;
template <typename SubjectT, typename U>
requires
std::is_constructible_v<Subject, SubjectT> &&
std::is_constructible_v<T, U>
constexpr with_directive_impl(SubjectT&& subject, U&& val)
noexcept(
std::is_nothrow_constructible_v<Subject, SubjectT> &&
std::is_nothrow_constructible_v<T, U>
)
: base_type(std::forward<SubjectT>(subject))
, val_(std::forward<U>(val))
{}
};
template <typename Subject, typename ID, typename T>
struct with_directive_impl<Subject, ID, T const>
: unary_parser<Subject, with_directive<Subject, ID, T const>>
{
using base_type = unary_parser<Subject, with_directive<Subject, ID, T const>>;
/* not mutable */ T const val_;
template <typename SubjectT, typename U>
requires
std::is_constructible_v<Subject, SubjectT> &&
std::is_constructible_v<T const, U>
constexpr with_directive_impl(SubjectT&& subject, U&& val)
noexcept(
std::is_nothrow_constructible_v<Subject, SubjectT> &&
std::is_nothrow_constructible_v<T const, U>
)
: base_type(std::forward<SubjectT>(subject))
, val_(std::forward<U>(val))
{}
};
template <typename Subject, typename ID, typename T>
struct with_directive_impl<Subject, ID, T&>
: unary_parser<Subject, with_directive<Subject, ID, T&>>
{
using base_type = unary_parser<Subject, with_directive<Subject, ID, T&>>;
T& val_;
template <typename SubjectT, typename U>
requires
std::is_constructible_v<Subject, SubjectT> &&
std::is_constructible_v<T&, U&>
constexpr with_directive_impl(SubjectT&& subject, U& val)
noexcept(std::is_nothrow_constructible_v<Subject, SubjectT>)
: base_type(std::forward<SubjectT>(subject))
, val_(val)
{}
};
} // detail
// `with` directive injects a value into the context prior to parsing.
// Holds lvalue references by reference, holds rvalue reference by value.
template <typename Subject, typename ID, typename T>
struct with_directive : detail::with_directive_impl<Subject, ID, T>
{
static_assert(
!std::is_rvalue_reference_v<T>,
"`x3::with`: rvalue reference is prohibited to prevent dangling reference"
);
static_assert(
std::is_lvalue_reference_v<T> || std::is_move_constructible_v<T>,
"Passing an rvalue to `x3::with` requires the type to be move-constructible "
"so it can be stored by value."
);
using subject_type = Subject;
using id_type = ID;
using value_type = T;
using base_type = detail::with_directive_impl<Subject, ID, T>;
static constexpr bool is_pass_through_unary = true;
static constexpr bool handles_container = Subject::handles_container;
template <typename SubjectT, typename U>
requires std::is_constructible_v<base_type, SubjectT, U>
constexpr with_directive(SubjectT&& subject, U&& val)
noexcept(std::is_nothrow_constructible_v<base_type, SubjectT, U>)
: base_type(std::forward<SubjectT>(subject), std::forward<U>(val))
{}
// The internal context type. This can be used to determine the composed
// context type used in `x3::parse`/`x3::phrase_parse`. It is required for
// the argument of `BOOST_SPIRIT_X3_INSTANTIATE`.
template <typename Context>
using context_t = context<ID, std::remove_reference_t<T>, Context>;
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
[[nodiscard]] constexpr bool
parse(It& first, Se const& last, Context const& context, RContext& rcontext, Attribute& attr) const
noexcept(is_nothrow_parsable_v<Subject, It, Se, context_t<Context>, RContext, Attribute>)
{
static_assert(Parsable<Subject, It, Se, context_t<Context>, RContext, Attribute>);
return this->subject.parse(
first, last
, make_context<ID>(this->val, context)
, rcontext
, attr);
first, last,
x3::make_context<ID>(this->val_, context),
rcontext, attr
);
}
private:
using base_type::val_;
};
template <typename ID, typename T>
struct with_gen
namespace detail
{
T&& val;
template <typename Subject>
constexpr with_directive<typename extension::as_parser<Subject>::value_type, ID, T>
operator[](Subject const& subject) const
template <typename ID, typename T>
struct [[nodiscard]] with_gen
{
return { as_parser(subject), std::forward<T>(val) };
}
};
static_assert(!std::is_rvalue_reference_v<T>);
T val;
template <typename ID, typename T>
constexpr with_gen<ID, T> with(T&& val)
template <X3Subject Subject>
[[nodiscard]] constexpr with_directive<as_parser_plain_t<Subject>, ID, T>
operator[](Subject&& subject) &&
noexcept(
is_parser_nothrow_castable_v<Subject> &&
std::is_nothrow_constructible_v<
with_directive<as_parser_plain_t<Subject>, ID, T>,
as_parser_t<Subject>,
T&& // lvalue or rvalue, forwarded
>
)
{
// with rvalue `with_gen`, the held value can always be forwarded
return { as_parser(std::forward<Subject>(subject)), std::forward<T>(val) };
}
template <X3Subject Subject>
[[nodiscard]] constexpr with_directive<as_parser_plain_t<Subject>, ID, T>
operator[](Subject&& subject) const&
noexcept(
is_parser_nothrow_castable_v<Subject> &&
std::is_nothrow_constructible_v<
with_directive<as_parser_plain_t<Subject>, ID, T>,
as_parser_t<Subject>,
T& // lvalue
>
)
{
static_assert(
std::is_lvalue_reference_v<T> || std::is_copy_constructible_v<T>,
"When you have passed an rvalue to `x3::with` and saved the functor "
"as a local variable, it requires the held type to be copy-constructible. "
"If your type is move only, then apply `std::move` to your `x3::with<ID>(val)` "
"instance."
);
return { as_parser(std::forward<Subject>(subject)), val };
}
};
template <typename ID>
struct with_fn
{
template <typename T>
static constexpr with_gen<ID, T> operator()(T&& val)
noexcept(
std::is_lvalue_reference_v<T> ||
std::is_nothrow_move_constructible_v<T>
)
{
return { std::forward<T>(val) };
}
};
} // detail
inline namespace cpos
{
return { std::forward<T>(val) };
}
}}}
// `with` directive injects a value into the context prior to parsing.
// Holds lvalue references by reference, holds rvalue reference by value.
template <typename ID>
inline constexpr detail::with_fn<ID> with{};
} // cpos
} // boost::spirit::x3
#endif

View File

@@ -9,7 +9,10 @@
#include <boost/spirit/home/x3.hpp>
#include <concepts>
#include <iterator>
#include <utility>
#include <type_traits>
namespace x3 = boost::spirit::x3;
@@ -33,27 +36,108 @@ struct my_rule_class
}
};
namespace
{
using boost::spirit::x3::rule;
using boost::spirit::x3::int_;
using boost::spirit::x3::with;
using boost::spirit::x3::_pass;
template<class T>
constexpr auto value_equals = int_[([](auto& ctx) {
auto&& with_val = x3::get<my_tag>(ctx);
static_assert(std::same_as<decltype(with_val), T>);
_pass(ctx) = with_val == _attr(ctx);
})];
struct move_only
{
move_only() = default;
move_only(move_only const&) = delete;
move_only(move_only&&) noexcept {}
move_only& operator=(move_only const&) = delete;
move_only& operator=(move_only&&) noexcept { return *this; }
};
} // anonymous
int main()
{
using spirit_test::test_attr;
using spirit_test::test;
using boost::spirit::x3::rule;
using boost::spirit::x3::int_;
using boost::spirit::x3::with;
// read from a mutable field is not allowed on these compilers
#if (!defined(_MSC_VER) || _MSC_VER >= 1910) && \
(!defined(__clang__) || __clang_major__ >= 7)
BOOST_SPIRIT_ASSERT_CONSTEXPR_CTORS(with<my_tag>(0)['x']);
#endif
{
constexpr int i = 0;
BOOST_SPIRIT_ASSERT_CONSTEXPR_CTORS(with<my_tag>(i)['x']);
}
{ // injecting data into the context in the grammar
// check various value categories
{
{
int i = 42;
BOOST_TEST(test("42", with<my_tag>(i)[value_equals<int&>]));
}
{
int const i = 42;
BOOST_TEST(test("42", with<my_tag>(i)[value_equals<int const&>]));
}
{
int i = 42;
BOOST_TEST(test("42", with<my_tag>(std::move(i))[value_equals<int&>]));
}
{
int const i = 42;
BOOST_TEST(test("42", with<my_tag>(std::move(i))[value_equals<int const&>]));
}
{
int i = 42;
auto with_gen = with<my_tag>(i);
BOOST_TEST(test("42", with_gen[value_equals<int&>]));
}
{
int const i = 42;
auto with_gen = with<my_tag>(i);
BOOST_TEST(test("42", with_gen[value_equals<int const&>]));
}
{
int i = 42;
auto with_gen = with<my_tag>(std::move(i));
BOOST_TEST(test("42", with_gen[value_equals<int&>]));
}
{
int const i = 42;
auto with_gen = with<my_tag>(std::move(i));
BOOST_TEST(test("42", with_gen[value_equals<int const&>]));
}
// lvalue `move_only`
{
move_only mo;
(void)with<my_tag>(mo)[int_];
}
{
move_only mo;
auto with_gen = with<my_tag>(mo); // passed-by-reference
(void)with_gen[int_]; // permitted, never copies
(void)std::move(with_gen)[int_];
}
// rvalue `move_only`
{
(void)with<my_tag>(move_only{})[int_];
}
{
auto with_gen = with<my_tag>(move_only{});
// (void)with_gen[int_]; // requires copy-constructible
(void)std::move(with_gen)[int_];
}
}
{
// injecting data into the context in the grammar
int val = 0;
auto r = rule<my_rule_class, char const*>() =
'(' > int_ > ',' > int_ > ')'
@@ -68,7 +152,8 @@ int main()
BOOST_TEST(val == 2);
}
{ // injecting non-const lvalue into the context
{
// injecting non-const lvalue into the context
int val = 0;
auto const r = int_[([](auto& ctx){
x3::get<my_tag>(ctx) += x3::_attr(ctx);
@@ -77,7 +162,8 @@ int main()
BOOST_TEST(579 == val);
}
{ // injecting rvalue into the context
{
// injecting rvalue into the context
auto const r1 = int_[([](auto& ctx){
x3::get<my_tag>(ctx) += x3::_attr(ctx);
})];
@@ -90,7 +176,8 @@ int main()
BOOST_TEST(106 == attr);
}
{ // injecting const/non-const lvalue and rvalue into the context
{
// injecting const/non-const lvalue and rvalue into the context
struct functor {
int operator()(int& val) {
return val * 10; // non-const ref returns 10 * injected val