1 // Copyright 2018 Hans Dembinski
3 // Distributed under the Boost Software License, Version 1.0.
4 // (See accompanying file LICENSE_1_0.txt
5 // or copy at http://www.boost.org/LICENSE_1_0.txt)
7 #ifndef BOOST_HISTOGRAM_AXIS_TRAITS_HPP
8 #define BOOST_HISTOGRAM_AXIS_TRAITS_HPP
10 #include <boost/core/ignore_unused.hpp>
11 #include <boost/histogram/axis/option.hpp>
12 #include <boost/histogram/detail/args_type.hpp>
13 #include <boost/histogram/detail/detect.hpp>
14 #include <boost/histogram/detail/priority.hpp>
15 #include <boost/histogram/detail/static_if.hpp>
16 #include <boost/histogram/detail/try_cast.hpp>
17 #include <boost/histogram/detail/type_name.hpp>
18 #include <boost/variant2/variant.hpp>
19 #include <boost/histogram/fwd.hpp>
20 #include <boost/mp11/algorithm.hpp>
21 #include <boost/mp11/list.hpp>
22 #include <boost/mp11/utility.hpp>
23 #include <boost/throw_exception.hpp>
33 struct value_type_deducer {
35 std::remove_cv_t<std::remove_reference_t<detail::arg_type<decltype(&Axis::index)>>>;
39 auto traits_options(priority<2>) -> axis::option::bitset<Axis::options()>;
42 auto traits_options(priority<1>) -> decltype(&Axis::update, axis::option::growth_t{});
45 auto traits_options(priority<0>) -> axis::option::none_t;
48 auto traits_is_inclusive(priority<1>) -> std::integral_constant<bool, Axis::inclusive()>;
51 auto traits_is_inclusive(priority<0>)
52 -> decltype(traits_options<Axis>(priority<2>{})
53 .test(axis::option::underflow | axis::option::overflow));
56 auto traits_is_ordered(priority<1>) -> std::integral_constant<bool, Axis::ordered()>;
58 template <class Axis, class ValueType = typename value_type_deducer<Axis>::type>
59 auto traits_is_ordered(priority<0>) -> typename std::is_arithmetic<ValueType>::type;
61 template <class I, class D, class A,
62 class J = std::decay_t<arg_type<decltype(&A::value)>>>
63 decltype(auto) value_method_switch(I&& i, D&& d, const A& a, priority<1>) {
64 return static_if<std::is_same<J, axis::index_type>>(std::forward<I>(i),
65 std::forward<D>(d), a);
68 template <class I, class D, class A>
69 double value_method_switch(I&&, D&&, const A&, priority<0>) {
70 // comma trick to make all compilers happy; some would complain about
71 // unreachable code after the throw, others about a missing return
72 return BOOST_THROW_EXCEPTION(
73 std::runtime_error(type_name<A>() + " has no value method")),
77 static axis::null_type null_value;
79 struct variant_access {
80 template <class T, class Variant>
81 static auto get_if(Variant* v) noexcept {
82 using T0 = mp11::mp_first<std::decay_t<Variant>>;
83 return static_if<std::is_pointer<T0>>(
85 using TP = mp11::mp_if<std::is_const<std::remove_pointer_t<T0>>, const T*, T*>;
86 auto ptp = variant2::get_if<TP>(vptr);
87 return ptp ? *ptp : nullptr;
89 [](auto* vptr) { return variant2::get_if<T>(vptr); }, &(v->impl));
92 template <class T0, class Visitor, class Variant>
93 static decltype(auto) visit_impl(mp11::mp_identity<T0>, Visitor&& vis, Variant&& v) {
94 return variant2::visit(std::forward<Visitor>(vis), v.impl);
97 template <class T0, class Visitor, class Variant>
98 static decltype(auto) visit_impl(mp11::mp_identity<T0*>, Visitor&& vis, Variant&& v) {
99 return variant2::visit(
100 [&vis](auto&& x) -> decltype(auto) { return std::forward<Visitor>(vis)(*x); },
104 template <class Visitor, class Variant>
105 static decltype(auto) visit(Visitor&& vis, Variant&& v) {
106 using T0 = mp11::mp_first<std::decay_t<Variant>>;
107 return visit_impl(mp11::mp_identity<T0>{}, std::forward<Visitor>(vis),
108 std::forward<Variant>(v));
112 } // namespace detail
117 /** Value type for axis type.
119 Doxygen does not render this well. This is a meta-function (template alias), it accepts
120 an axis type and returns the value type.
122 The value type is deduced from the argument of the `Axis::index` method. Const
123 references are decayed to the their value types, for example, the type deduced for
124 `Axis::index(const int&)` is `int`.
126 The deduction always succeeds if the axis type models the Axis concept correctly. Errors
127 come from violations of the concept, in particular, an index method that is templated or
128 overloaded is not allowed.
130 @tparam Axis axis type.
132 template <class Axis>
133 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
134 using value_type = typename detail::value_type_deducer<Axis>::type;
139 /** Whether axis is continuous or discrete.
141 Doxygen does not render this well. This is a meta-function (template alias), it accepts
142 an axis type and returns a compile-time boolean.
144 If the boolean is true, the axis is continuous (covers a continuous range of values).
145 Otherwise it is discrete (covers discrete values).
147 template <class Axis>
148 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
149 using is_continuous = typename std::is_floating_point<traits::value_type<Axis>>::type;
151 struct is_continuous;
154 /** Meta-function to detect whether an axis is reducible.
156 Doxygen does not render this well. This is a meta-function (template alias), it accepts
157 an axis type and represents compile-time boolean which is true or false, depending on
158 whether the axis can be reduced with boost::histogram::algorithm::reduce().
160 An axis can be made reducible by adding a special constructor, see Axis concept for
163 @tparam Axis axis type.
165 template <class Axis>
166 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
167 using is_reducible = std::is_constructible<Axis, const Axis&, axis::index_type,
168 axis::index_type, unsigned>;
173 /** Get axis options for axis type.
175 Doxygen does not render this well. This is a meta-function (template alias), it accepts
176 an axis type and returns the boost::histogram::axis::option::bitset.
178 If Axis::options() is valid and constexpr, get_options is the corresponding
179 option type. Otherwise, it is boost::histogram::axis::option::growth_t, if the
180 axis has a method `update`, else boost::histogram::axis::option::none_t.
182 @tparam Axis axis type
184 template <class Axis>
185 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
186 using get_options = decltype(detail::traits_options<Axis>(detail::priority<2>{}));
188 template <class Axis>
189 using static_options [[deprecated("use get_options instead")]] = get_options<Axis>;
195 /** Meta-function to detect whether an axis is inclusive.
197 Doxygen does not render this well. This is a meta-function (template alias), it accepts
198 an axis type and represents compile-time boolean which is true or false, depending on
199 whether the axis is inclusive or not.
201 An axis with underflow and overflow bins is always inclusive, but an axis may be
202 inclusive under other conditions. The meta-function checks for the method `constexpr
203 static bool inclusive()`, and uses the result. If this method is not present, it uses
204 get_options<Axis> and checks whether the underflow and overflow bits are present.
206 An inclusive axis has a bin for every possible input value. A histogram which consists
207 only of inclusive axes can be filled more efficiently, since input values always
208 end up in a valid cell and there is no need to keep track of input tuples that need to
211 @tparam Axis axis type
213 template <class Axis>
214 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
215 using is_inclusive = decltype(detail::traits_is_inclusive<Axis>(detail::priority<1>{}));
217 template <class Axis>
218 using static_is_inclusive [[deprecated("use is_inclusive instead")]] = is_inclusive<Axis>;
224 /** Meta-function to detect whether an axis is ordered.
226 Doxygen does not render this well. This is a meta-function (template alias), it accepts
227 an axis type and returns a compile-time boolean. If the boolean is true, the axis is
230 The meta-function checks for the method `constexpr static bool ordered()`, and uses the
231 result. If this method is not present, it returns true if the value type of the Axis is
232 arithmetic and false otherwise.
234 An ordered axis has a value type that is ordered, which means that indices i <
235 j < k implies either value(i) < value(j) < value(k) or value(i) > value(j) > value(k)
236 for all i,j,k. For example, the integer axis is ordered, but the category axis is not.
237 Axis which are not ordered must not have underflow bins, because they only have an
238 "other" category, which is identified with the overflow bin if it is available.
240 @tparam Axis axis type
242 template <class Axis>
243 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
244 using is_ordered = decltype(detail::traits_is_ordered<Axis>(detail::priority<1>{}));
249 /** Returns axis options as unsigned integer.
251 See get_options for details.
253 @param axis any axis instance
255 template <class Axis>
256 constexpr unsigned options(const Axis& axis) noexcept {
257 boost::ignore_unused(axis);
258 return get_options<Axis>::value;
261 // specialization for variant
262 template <class... Ts>
263 unsigned options(const variant<Ts...>& axis) noexcept {
264 return axis.options();
267 /** Returns true if axis is inclusive or false.
269 See is_inclusive for details.
271 @param axis any axis instance
273 template <class Axis>
274 constexpr bool inclusive(const Axis& axis) noexcept {
275 boost::ignore_unused(axis);
276 return is_inclusive<Axis>::value;
279 // specialization for variant
280 template <class... Ts>
281 bool inclusive(const variant<Ts...>& axis) noexcept {
282 return axis.inclusive();
285 /** Returns true if axis is ordered or false.
287 See is_ordered for details.
289 @param axis any axis instance
291 template <class Axis>
292 constexpr bool ordered(const Axis& axis) noexcept {
293 boost::ignore_unused(axis);
294 return is_ordered<Axis>::value;
297 // specialization for variant
298 template <class... Ts>
299 bool ordered(const variant<Ts...>& axis) noexcept {
300 return axis.ordered();
303 /** Returns axis size plus any extra bins for under- and overflow.
305 @param axis any axis instance
307 template <class Axis>
308 index_type extent(const Axis& axis) noexcept {
309 const auto opt = options(axis);
310 return axis.size() + (opt & option::underflow ? 1 : 0) +
311 (opt & option::overflow ? 1 : 0);
314 /** Returns reference to metadata of an axis.
316 If the expression x.metadata() for an axis instance `x` (maybe const) is valid, return
317 the result. Otherwise, return a reference to a static instance of
318 boost::histogram::axis::null_type.
320 @param axis any axis instance
322 template <class Axis>
323 decltype(auto) metadata(Axis&& axis) noexcept {
324 return detail::static_if<detail::has_method_metadata<std::decay_t<Axis>>>(
325 [](auto&& a) -> decltype(auto) { return a.metadata(); },
326 [](auto &&) -> mp11::mp_if<std::is_const<std::remove_reference_t<Axis>>,
327 axis::null_type const&, axis::null_type&> {
328 return detail::null_value;
330 std::forward<Axis>(axis));
333 /** Returns axis value for index.
335 If the axis has no `value` method, throw std::runtime_error. If the method exists and
336 accepts a floating point index, pass the index and return the result. If the method
337 exists but accepts only integer indices, cast the floating point index to int, pass this
338 index and return the result.
340 @param axis any axis instance
341 @param index floating point axis index
343 template <class Axis>
344 decltype(auto) value(const Axis& axis, real_index_type index) {
345 return detail::value_method_switch(
346 [index](const auto& a) { return a.value(static_cast<index_type>(index)); },
347 [index](const auto& a) { return a.value(index); }, axis, detail::priority<1>{});
350 /** Returns axis value for index if it is convertible to target type or throws.
352 Like boost::histogram::axis::traits::value, but converts the result into the requested
353 return type. If the conversion is not possible, throws std::runtime_error.
355 @tparam Result requested return type
356 @tparam Axis axis type
357 @param axis any axis instance
358 @param index floating point axis index
360 template <class Result, class Axis>
361 Result value_as(const Axis& axis, real_index_type index) {
362 return detail::try_cast<Result, std::runtime_error>(
363 value(axis, index)); // avoid conversion warning
366 /** Returns axis index for value.
368 Throws std::invalid_argument if the value argument is not implicitly convertible.
370 @param axis any axis instance
371 @param value argument to be passed to `index` method
373 template <class Axis, class U>
374 axis::index_type index(const Axis& axis, const U& value) noexcept(
375 std::is_convertible<U, value_type<Axis>>::value) {
376 return axis.index(detail::try_cast<value_type<Axis>, std::invalid_argument>(value));
379 // specialization for variant
380 template <class... Ts, class U>
381 axis::index_type index(const variant<Ts...>& axis, const U& value) {
382 return axis.index(value);
385 /** Return axis rank (how many arguments it processes).
387 @param axis any axis instance
389 template <class Axis>
390 constexpr unsigned rank(const Axis& axis) {
391 boost::ignore_unused(axis);
392 using T = value_type<Axis>;
393 // cannot use mp_eval_or since T could be a fixed-sized sequence
394 return mp11::mp_eval_if_not<detail::is_tuple<T>, mp11::mp_size_t<1>, mp11::mp_size,
398 // specialization for variant
399 template <class... Ts>
400 unsigned rank(const axis::variant<Ts...>& axis) {
401 return detail::variant_access::visit([](const auto& a) { return rank(a); }, axis);
404 /** Returns pair of axis index and shift for the value argument.
406 Throws `std::invalid_argument` if the value argument is not implicitly convertible to
407 the argument expected by the `index` method. If the result of
408 boost::histogram::axis::traits::get_options<decltype(axis)> has the growth flag set,
409 call `update` method with the argument and return the result. Otherwise, call `index`
410 and return the pair of the result and a zero shift.
412 @param axis any axis instance
413 @param value argument to be passed to `update` or `index` method
415 template <class Axis, class U>
416 std::pair<index_type, index_type> update(Axis& axis, const U& value) noexcept(
417 std::is_convertible<U, value_type<Axis>>::value) {
418 return detail::static_if_c<get_options<Axis>::test(option::growth)>(
420 return a.update(detail::try_cast<value_type<Axis>, std::invalid_argument>(value));
422 [&value](auto& a) -> std::pair<index_type, index_type> {
423 return {index(a, value), 0};
428 // specialization for variant
429 template <class... Ts, class U>
430 std::pair<index_type, index_type> update(variant<Ts...>& axis, const U& value) {
431 return visit([&value](auto& a) { return a.update(value); }, axis);
434 /** Returns bin width at axis index.
436 If the axis has no `value` method, throw std::runtime_error. If the method exists and
437 accepts a floating point index, return the result of `axis.value(index + 1) -
438 axis.value(index)`. If the method exists but accepts only integer indices, return 0.
440 @param axis any axis instance
441 @param index bin index
443 template <class Axis>
444 decltype(auto) width(const Axis& axis, index_type index) {
445 return detail::value_method_switch(
446 [](const auto&) { return 0; },
447 [index](const auto& a) { return a.value(index + 1) - a.value(index); }, axis,
448 detail::priority<1>{});
451 /** Returns bin width at axis index.
453 Like boost::histogram::axis::traits::width, but converts the result into the requested
454 return type. If the conversion is not possible, throw std::runtime_error.
456 @param axis any axis instance
457 @param index bin index
459 template <class Result, class Axis>
460 Result width_as(const Axis& axis, index_type index) {
461 return detail::value_method_switch(
462 [](const auto&) { return Result{}; },
463 [index](const auto& a) {
464 return detail::try_cast<Result, std::runtime_error>(a.value(index + 1) -
467 axis, detail::priority<1>{});
470 } // namespace traits
472 } // namespace histogram