1 // Copyright 2015-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_HISTOGRAM_HPP
8 #define BOOST_HISTOGRAM_HISTOGRAM_HPP
10 #include <boost/histogram/detail/accumulator_traits.hpp>
11 #include <boost/histogram/detail/argument_traits.hpp>
12 #include <boost/histogram/detail/at.hpp>
13 #include <boost/histogram/detail/axes.hpp>
14 #include <boost/histogram/detail/common_type.hpp>
15 #include <boost/histogram/detail/fill.hpp>
16 #include <boost/histogram/detail/fill_n.hpp>
17 #include <boost/histogram/detail/mutex_base.hpp>
18 #include <boost/histogram/detail/non_member_container_access.hpp>
19 #include <boost/histogram/detail/span.hpp>
20 #include <boost/histogram/fwd.hpp>
21 #include <boost/histogram/sample.hpp>
22 #include <boost/histogram/storage_adaptor.hpp>
23 #include <boost/histogram/unsafe_access.hpp>
24 #include <boost/histogram/weight.hpp>
25 #include <boost/mp11/integral.hpp>
26 #include <boost/mp11/list.hpp>
27 #include <boost/mp11/tuple.hpp>
28 #include <boost/throw_exception.hpp>
32 #include <type_traits>
39 /** Central class of the histogram library.
41 Histogram uses the call operator to insert data, like the
42 [Boost.Accumulators](https://www.boost.org/doc/libs/develop/doc/html/accumulators.html).
44 Use factory functions (see
45 [make_histogram.hpp](histogram/reference.html#header.boost.histogram.make_histogram_hpp)
47 [make_profile.hpp](histogram/reference.html#header.boost.histogram.make_profile_hpp)) to
48 conveniently create histograms rather than calling the ctors directly.
50 Use the [indexed](boost/histogram/indexed.html) range generator to iterate over filled
51 histograms, which is convenient and faster than hand-written loops for multi-dimensional
54 @tparam Axes std::tuple of axis types OR std::vector of an axis type or axis::variant
55 @tparam Storage class that implements the storage interface
57 template <class Axes, class Storage>
58 class histogram : detail::mutex_base<Axes, Storage> {
59 static_assert(std::is_same<std::decay_t<Storage>, Storage>::value,
60 "Storage may not be a reference or const or volatile");
61 static_assert(mp11::mp_size<Axes>::value > 0, "at least one axis required");
64 using axes_type = Axes;
65 using storage_type = Storage;
66 using value_type = typename storage_type::value_type;
67 // typedefs for boost::range_iterator
68 using iterator = typename storage_type::iterator;
69 using const_iterator = typename storage_type::const_iterator;
72 using mutex_base = typename detail::mutex_base<axes_type, storage_type>;
75 histogram() = default;
77 template <class A, class S>
78 explicit histogram(histogram<A, S>&& rhs)
79 : storage_(std::move(unsafe_access::storage(rhs)))
80 , offset_(unsafe_access::offset(rhs)) {
81 detail::axes_assign(axes_, std::move(unsafe_access::axes(rhs)));
82 detail::throw_if_axes_is_too_large(axes_);
85 template <class A, class S>
86 explicit histogram(const histogram<A, S>& rhs)
87 : storage_(unsafe_access::storage(rhs)), offset_(unsafe_access::offset(rhs)) {
88 detail::axes_assign(axes_, unsafe_access::axes(rhs));
89 detail::throw_if_axes_is_too_large(axes_);
92 template <class A, class S>
93 histogram& operator=(histogram<A, S>&& rhs) {
94 detail::axes_assign(axes_, std::move(unsafe_access::axes(rhs)));
95 detail::throw_if_axes_is_too_large(axes_);
96 storage_ = std::move(unsafe_access::storage(rhs));
97 offset_ = unsafe_access::offset(rhs);
101 template <class A, class S>
102 histogram& operator=(const histogram<A, S>& rhs) {
103 detail::axes_assign(axes_, unsafe_access::axes(rhs));
104 detail::throw_if_axes_is_too_large(axes_);
105 storage_ = unsafe_access::storage(rhs);
106 offset_ = unsafe_access::offset(rhs);
110 template <class A, class = detail::requires_axes<A>>
111 histogram(A&& a, Storage s)
112 : axes_(std::forward<A>(a))
113 , storage_(std::move(s))
114 , offset_(detail::offset(axes_)) {
115 detail::throw_if_axes_is_too_large(axes_);
116 storage_.reset(detail::bincount(axes_));
119 explicit histogram(Axes axes) : histogram(axes, storage_type()) {}
121 template <class... As, class = detail::requires_axes<std::tuple<std::decay_t<As>...>>>
122 explicit histogram(As&&... as)
123 : histogram(std::tuple<std::decay_t<As>...>(std::forward<As>(as)...),
126 /// Number of axes (dimensions).
127 constexpr unsigned rank() const noexcept { return detail::axes_rank(axes_); }
129 /// Total number of bins (including underflow/overflow).
130 std::size_t size() const noexcept { return storage_.size(); }
132 /// Reset all bins to default initialized values.
133 void reset() { storage_.reset(size()); }
135 /// Get N-th axis using a compile-time number.
136 /// This version is more efficient than the one accepting a run-time number.
137 template <unsigned N = 0>
138 decltype(auto) axis(std::integral_constant<unsigned, N> = {}) const {
139 detail::axis_index_is_valid(axes_, N);
140 return detail::axis_get<N>(axes_);
143 /// Get N-th axis with run-time number.
144 /// Prefer the version that accepts a compile-time number, if you can use it.
145 decltype(auto) axis(unsigned i) const {
146 detail::axis_index_is_valid(axes_, i);
147 return detail::axis_get(axes_, i);
150 /// Apply unary functor/function to each axis.
151 template <class Unary>
152 auto for_each_axis(Unary&& unary) const {
153 return detail::for_each_axis(axes_, std::forward<Unary>(unary));
156 /** Fill histogram with values, an optional weight, and/or a sample.
158 Arguments are passed in order to the axis objects. Passing an argument type that is
159 not convertible to the value type accepted by the axis or passing the wrong number
160 of arguments causes a throw of `std::invalid_argument`.
164 An optional weight can be passed as the first or last argument
165 with the [weight](boost/histogram/weight.html) helper function. Compilation fails if
166 the storage elements do not support weights.
170 If the storage elements accept samples, pass them with the sample helper function
171 in addition to the axis arguments, which can be the first or last argument. The
172 [sample](boost/histogram/sample.html) helper function can pass one or more arguments
173 to the storage element. If samples and weights are used together, they can be passed
174 in any order at the beginning or end of the argument list.
176 __Axis with multiple arguments__
178 If the histogram contains an axis which accepts a `std::tuple` of arguments, the
179 arguments for that axis need to passed as a `std::tuple`, for example,
180 `std::make_tuple(1.2, 2.3)`. If the histogram contains only this axis and no other,
181 the arguments can be passed directly.
183 template <class Arg0, class... Args>
184 std::enable_if_t<(detail::is_tuple<Arg0>::value == false || sizeof...(Args) > 0),
186 operator()(const Arg0& arg0, const Args&... args) {
187 return operator()(std::forward_as_tuple(arg0, args...));
190 /// Fill histogram with values, an optional weight, and/or a sample from a `std::tuple`.
191 template <class... Ts>
192 iterator operator()(const std::tuple<Ts...>& args) {
193 using arg_traits = detail::argument_traits<std::decay_t<Ts>...>;
194 using acc_traits = detail::accumulator_traits<value_type>;
195 constexpr bool weight_valid =
196 arg_traits::wpos::value == -1 || acc_traits::wsupport::value;
197 static_assert(weight_valid, "error: accumulator does not support weights");
198 detail::sample_args_passed_vs_expected<typename arg_traits::sargs,
199 typename acc_traits::args>();
200 constexpr bool sample_valid =
201 std::is_convertible<typename arg_traits::sargs, typename acc_traits::args>::value;
202 std::lock_guard<typename mutex_base::type> guard{mutex_base::get()};
203 return detail::fill(mp11::mp_bool<(weight_valid && sample_valid)>{}, arg_traits{},
204 offset_, storage_, axes_, args);
207 /** Fill histogram with several values at once.
209 The argument must be an iterable with a size that matches the
210 rank of the histogram. The element of an iterable may be 1) a value or 2) an iterable
211 with contiguous storage over values or 3) a variant of 1) and 2). Sub-iterables must
212 have the same length.
214 Values are passed to the corresponding histogram axis in order. If a single value is
215 passed together with an iterable of values, the single value is treated like an
216 iterable with matching length of copies of this value.
218 If the histogram has only one axis, an iterable of values may be passed directly.
220 @param args iterable as explained in the long description.
222 template <class Iterable, class = detail::requires_iterable<Iterable>>
223 void fill(const Iterable& args) {
224 using acc_traits = detail::accumulator_traits<value_type>;
225 constexpr unsigned n_sample_args_expected =
226 std::tuple_size<typename acc_traits::args>::value;
227 static_assert(n_sample_args_expected == 0,
228 "sample argument is missing but required by accumulator");
229 std::lock_guard<typename mutex_base::type> guard{mutex_base::get()};
230 detail::fill_n(mp11::mp_bool<(n_sample_args_expected == 0)>{}, offset_, storage_,
231 axes_, detail::make_span(args));
234 /** Fill histogram with several values and weights at once.
236 @param args iterable of values.
237 @param weights single weight or an iterable of weights.
239 template <class Iterable, class T, class = detail::requires_iterable<Iterable>>
240 void fill(const Iterable& args, const weight_type<T>& weights) {
241 using acc_traits = detail::accumulator_traits<value_type>;
242 constexpr bool weight_valid = acc_traits::wsupport::value;
243 static_assert(weight_valid, "error: accumulator does not support weights");
244 detail::sample_args_passed_vs_expected<std::tuple<>, typename acc_traits::args>();
245 constexpr bool sample_valid =
246 std::is_convertible<std::tuple<>, typename acc_traits::args>::value;
247 std::lock_guard<typename mutex_base::type> guard{mutex_base::get()};
248 detail::fill_n(mp11::mp_bool<(weight_valid && sample_valid)>{}, offset_, storage_,
249 axes_, detail::make_span(args),
250 weight(detail::to_ptr_size(weights.value)));
253 /** Fill histogram with several values and weights at once.
255 @param weights single weight or an iterable of weights.
256 @param args iterable of values.
258 template <class Iterable, class T, class = detail::requires_iterable<Iterable>>
259 void fill(const weight_type<T>& weights, const Iterable& args) {
263 /** Fill histogram with several values and samples at once.
265 @param args iterable of values.
266 @param samples single sample or an iterable of samples.
268 template <class Iterable, class... Ts, class = detail::requires_iterable<Iterable>>
269 void fill(const Iterable& args, const sample_type<std::tuple<Ts...>>& samples) {
270 using acc_traits = detail::accumulator_traits<value_type>;
271 using sample_args_passed =
272 std::tuple<decltype(*detail::to_ptr_size(std::declval<Ts>()).first)...>;
273 detail::sample_args_passed_vs_expected<sample_args_passed,
274 typename acc_traits::args>();
275 std::lock_guard<typename mutex_base::type> guard{mutex_base::get()};
277 [&](const auto&... sargs) {
278 constexpr bool sample_valid =
279 std::is_convertible<sample_args_passed, typename acc_traits::args>::value;
280 detail::fill_n(mp11::mp_bool<(sample_valid)>{}, offset_, storage_, axes_,
281 detail::make_span(args), detail::to_ptr_size(sargs)...);
286 /** Fill histogram with several values and samples at once.
288 @param samples single sample or an iterable of samples.
289 @param args iterable of values.
291 template <class Iterable, class T, class = detail::requires_iterable<Iterable>>
292 void fill(const sample_type<T>& samples, const Iterable& args) {
296 template <class Iterable, class T, class... Ts,
297 class = detail::requires_iterable<Iterable>>
298 void fill(const Iterable& args, const weight_type<T>& weights,
299 const sample_type<std::tuple<Ts...>>& samples) {
300 using acc_traits = detail::accumulator_traits<value_type>;
301 using sample_args_passed =
302 std::tuple<decltype(*detail::to_ptr_size(std::declval<Ts>()).first)...>;
303 detail::sample_args_passed_vs_expected<sample_args_passed,
304 typename acc_traits::args>();
305 std::lock_guard<typename mutex_base::type> guard{mutex_base::get()};
307 [&](const auto&... sargs) {
308 constexpr bool weight_valid = acc_traits::wsupport::value;
309 static_assert(weight_valid, "error: accumulator does not support weights");
310 constexpr bool sample_valid =
311 std::is_convertible<sample_args_passed, typename acc_traits::args>::value;
312 detail::fill_n(mp11::mp_bool<(weight_valid && sample_valid)>{}, offset_,
313 storage_, axes_, detail::make_span(args),
314 weight(detail::to_ptr_size(weights.value)),
315 detail::to_ptr_size(sargs)...);
320 template <class Iterable, class T, class U, class = detail::requires_iterable<Iterable>>
321 void fill(const sample_type<T>& samples, const weight_type<U>& weights,
322 const Iterable& args) {
323 fill(args, weights, samples);
326 template <class Iterable, class T, class U, class = detail::requires_iterable<Iterable>>
327 void fill(const weight_type<T>& weights, const sample_type<U>& samples,
328 const Iterable& args) {
329 fill(args, weights, samples);
332 template <class Iterable, class T, class U, class = detail::requires_iterable<Iterable>>
333 void fill(const Iterable& args, const sample_type<T>& samples,
334 const weight_type<U>& weights) {
335 fill(args, weights, samples);
338 /** Access cell value at integral indices.
340 You can pass indices as individual arguments, as a std::tuple of integers, or as an
341 interable range of integers. Passing the wrong number of arguments causes a throw of
342 std::invalid_argument. Passing an index which is out of bounds causes a throw of
345 @param i index of first axis.
346 @param is indices of second, third, ... axes.
347 @returns reference to cell value.
349 template <class... Indices>
350 decltype(auto) at(axis::index_type i, Indices... is) {
351 return at(std::forward_as_tuple(i, is...));
354 /// Access cell value at integral indices (read-only).
355 template <class... Indices>
356 decltype(auto) at(axis::index_type i, Indices... is) const {
357 return at(std::forward_as_tuple(i, is...));
360 /// Access cell value at integral indices stored in `std::tuple`.
361 template <class... Indices>
362 decltype(auto) at(const std::tuple<Indices...>& is) {
363 if (rank() != sizeof...(Indices))
364 BOOST_THROW_EXCEPTION(
365 std::invalid_argument("number of arguments != histogram rank"));
366 const auto idx = detail::at(axes_, is);
368 BOOST_THROW_EXCEPTION(std::out_of_range("at least one index out of bounds"));
369 BOOST_ASSERT(idx < storage_.size());
370 return storage_[idx];
373 /// Access cell value at integral indices stored in `std::tuple` (read-only).
374 template <typename... Indices>
375 decltype(auto) at(const std::tuple<Indices...>& is) const {
376 if (rank() != sizeof...(Indices))
377 BOOST_THROW_EXCEPTION(
378 std::invalid_argument("number of arguments != histogram rank"));
379 const auto idx = detail::at(axes_, is);
381 BOOST_THROW_EXCEPTION(std::out_of_range("at least one index out of bounds"));
382 BOOST_ASSERT(idx < storage_.size());
383 return storage_[idx];
386 /// Access cell value at integral indices stored in iterable.
387 template <class Iterable, class = detail::requires_iterable<Iterable>>
388 decltype(auto) at(const Iterable& is) {
389 if (rank() != detail::axes_rank(is))
390 BOOST_THROW_EXCEPTION(
391 std::invalid_argument("number of arguments != histogram rank"));
392 const auto idx = detail::at(axes_, is);
394 BOOST_THROW_EXCEPTION(std::out_of_range("at least one index out of bounds"));
395 BOOST_ASSERT(idx < storage_.size());
396 return storage_[idx];
399 /// Access cell value at integral indices stored in iterable (read-only).
400 template <class Iterable, class = detail::requires_iterable<Iterable>>
401 decltype(auto) at(const Iterable& is) const {
402 if (rank() != detail::axes_rank(is))
403 BOOST_THROW_EXCEPTION(
404 std::invalid_argument("number of arguments != histogram rank"));
405 const auto idx = detail::at(axes_, is);
407 BOOST_THROW_EXCEPTION(std::out_of_range("at least one index out of bounds"));
408 BOOST_ASSERT(idx < storage_.size());
409 return storage_[idx];
412 /// Access value at index (number for rank = 1, else `std::tuple` or iterable).
413 template <class Indices>
414 decltype(auto) operator[](const Indices& is) {
418 /// Access value at index (read-only).
419 template <class Indices>
420 decltype(auto) operator[](const Indices& is) const {
424 /// Equality operator, tests equality for all axes and the storage.
425 template <class A, class S>
426 bool operator==(const histogram<A, S>& rhs) const noexcept {
427 // testing offset is redundant, but offers fast return if it fails
428 return offset_ == unsafe_access::offset(rhs) &&
429 detail::axes_equal(axes_, unsafe_access::axes(rhs)) &&
430 storage_ == unsafe_access::storage(rhs);
433 /// Negation of the equality operator.
434 template <class A, class S>
435 bool operator!=(const histogram<A, S>& rhs) const noexcept {
436 return !operator==(rhs);
439 /// Add values of another histogram.
440 template <class A, class S>
442 detail::has_operator_radd<value_type, typename histogram<A, S>::value_type>::value,
444 operator+=(const histogram<A, S>& rhs) {
445 if (!detail::axes_equal(axes_, unsafe_access::axes(rhs)))
446 BOOST_THROW_EXCEPTION(std::invalid_argument("axes of histograms differ"));
447 auto rit = unsafe_access::storage(rhs).begin();
448 for (auto&& x : storage_) x += *rit++;
452 /// Subtract values of another histogram.
453 template <class A, class S>
455 detail::has_operator_rsub<value_type, typename histogram<A, S>::value_type>::value,
457 operator-=(const histogram<A, S>& rhs) {
458 if (!detail::axes_equal(axes_, unsafe_access::axes(rhs)))
459 BOOST_THROW_EXCEPTION(std::invalid_argument("axes of histograms differ"));
460 auto rit = unsafe_access::storage(rhs).begin();
461 for (auto&& x : storage_) x -= *rit++;
465 /// Multiply by values of another histogram.
466 template <class A, class S>
468 detail::has_operator_rmul<value_type, typename histogram<A, S>::value_type>::value,
470 operator*=(const histogram<A, S>& rhs) {
471 if (!detail::axes_equal(axes_, unsafe_access::axes(rhs)))
472 BOOST_THROW_EXCEPTION(std::invalid_argument("axes of histograms differ"));
473 auto rit = unsafe_access::storage(rhs).begin();
474 for (auto&& x : storage_) x *= *rit++;
478 /// Divide by values of another histogram.
479 template <class A, class S>
481 detail::has_operator_rdiv<value_type, typename histogram<A, S>::value_type>::value,
483 operator/=(const histogram<A, S>& rhs) {
484 if (!detail::axes_equal(axes_, unsafe_access::axes(rhs)))
485 BOOST_THROW_EXCEPTION(std::invalid_argument("axes of histograms differ"));
486 auto rit = unsafe_access::storage(rhs).begin();
487 for (auto&& x : storage_) x /= *rit++;
491 /// Multiply all values with a scalar.
492 template <class V = value_type>
493 std::enable_if_t<(detail::has_operator_rmul<V, double>::value &&
494 detail::has_operator_rmul<storage_type, double>::value == true),
496 operator*=(const double x) {
497 // use special implementation of scaling if available
502 /// Multiply all values with a scalar.
503 template <class V = value_type>
504 std::enable_if_t<(detail::has_operator_rmul<V, double>::value &&
505 detail::has_operator_rmul<storage_type, double>::value == false),
507 operator*=(const double x) {
508 // generic implementation of scaling
509 for (auto&& si : storage_) si *= x;
513 /// Divide all values by a scalar.
514 template <class V = value_type>
515 std::enable_if_t<detail::has_operator_rmul<V, double>::value, histogram&> operator/=(
517 return operator*=(1.0 / x);
520 /// Return value iterator to the beginning of the histogram.
521 iterator begin() noexcept { return storage_.begin(); }
523 /// Return value iterator to the end in the histogram.
524 iterator end() noexcept { return storage_.end(); }
526 /// Return value iterator to the beginning of the histogram (read-only).
527 const_iterator begin() const noexcept { return storage_.begin(); }
529 /// Return value iterator to the end in the histogram (read-only).
530 const_iterator end() const noexcept { return storage_.end(); }
532 /// Return value iterator to the beginning of the histogram (read-only).
533 const_iterator cbegin() const noexcept { return begin(); }
535 /// Return value iterator to the end in the histogram (read-only).
536 const_iterator cend() const noexcept { return end(); }
538 template <class Archive>
539 void serialize(Archive& ar, unsigned /* version */) {
540 detail::axes_serialize(ar, axes_);
541 ar& make_nvp("storage", storage_);
542 if (Archive::is_loading::value) {
543 offset_ = detail::offset(axes_);
544 detail::throw_if_axes_is_too_large(axes_);
550 storage_type storage_;
551 std::size_t offset_ = 0;
553 friend struct unsafe_access;
557 Pairwise add cells of two histograms and return histogram with the sum.
559 The returned histogram type is the most efficient and safest one constructible from the
560 inputs, if they are not the same type. If one histogram has a tuple axis, the result has
561 a tuple axis. The chosen storage is the one with the larger dynamic range.
563 template <class A1, class S1, class A2, class S2>
564 auto operator+(const histogram<A1, S1>& a, const histogram<A2, S2>& b) {
565 auto r = histogram<detail::common_axes<A1, A2>, detail::common_storage<S1, S2>>(a);
569 /** Pairwise multiply cells of two histograms and return histogram with the product.
571 For notes on the returned histogram type, see operator+.
573 template <class A1, class S1, class A2, class S2>
574 auto operator*(const histogram<A1, S1>& a, const histogram<A2, S2>& b) {
575 auto r = histogram<detail::common_axes<A1, A2>, detail::common_storage<S1, S2>>(a);
579 /** Pairwise subtract cells of two histograms and return histogram with the difference.
581 For notes on the returned histogram type, see operator+.
583 template <class A1, class S1, class A2, class S2>
584 auto operator-(const histogram<A1, S1>& a, const histogram<A2, S2>& b) {
585 auto r = histogram<detail::common_axes<A1, A2>, detail::common_storage<S1, S2>>(a);
589 /** Pairwise divide cells of two histograms and return histogram with the quotient.
591 For notes on the returned histogram type, see operator+.
593 template <class A1, class S1, class A2, class S2>
594 auto operator/(const histogram<A1, S1>& a, const histogram<A2, S2>& b) {
595 auto r = histogram<detail::common_axes<A1, A2>, detail::common_storage<S1, S2>>(a);
599 /** Multiply all cells of the histogram by a number and return a new histogram.
601 If the original histogram has integer cells, the result has double cells.
603 template <class A, class S>
604 auto operator*(const histogram<A, S>& h, double x) {
605 auto r = histogram<A, detail::common_storage<S, dense_storage<double>>>(h);
609 /** Multiply all cells of the histogram by a number and return a new histogram.
611 If the original histogram has integer cells, the result has double cells.
613 template <class A, class S>
614 auto operator*(double x, const histogram<A, S>& h) {
618 /** Divide all cells of the histogram by a number and return a new histogram.
620 If the original histogram has integer cells, the result has double cells.
622 template <class A, class S>
623 auto operator/(const histogram<A, S>& h, double x) {
624 return h * (1.0 / x);
627 #if __cpp_deduction_guides >= 201606
629 template <class... Axes, class = detail::requires_axes<std::tuple<std::decay_t<Axes>...>>>
630 histogram(Axes...)->histogram<std::tuple<std::decay_t<Axes>...>>;
632 template <class... Axes, class S, class = detail::requires_storage_or_adaptible<S>>
633 histogram(std::tuple<Axes...>, S)
634 ->histogram<std::tuple<Axes...>, std::conditional_t<detail::is_adaptible<S>::value,
635 storage_adaptor<S>, S>>;
637 template <class Iterable, class = detail::requires_iterable<Iterable>,
638 class = detail::requires_any_axis<typename Iterable::value_type>>
639 histogram(Iterable)->histogram<std::vector<typename Iterable::value_type>>;
641 template <class Iterable, class S, class = detail::requires_iterable<Iterable>,
642 class = detail::requires_any_axis<typename Iterable::value_type>,
643 class = detail::requires_storage_or_adaptible<S>>
644 histogram(Iterable, S)
646 std::vector<typename Iterable::value_type>,
647 std::conditional_t<detail::is_adaptible<S>::value, storage_adaptor<S>, S>>;
651 } // namespace histogram