Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-25 09:28:14

0001 // Author: Enrico Guiraud, Danilo Piparo CERN  03/2017
0002 
0003 /*************************************************************************
0004  * Copyright (C) 1995-2021, Rene Brun and Fons Rademakers.               *
0005  * All rights reserved.                                                  *
0006  *                                                                       *
0007  * For the licensing terms see $ROOTSYS/LICENSE.                         *
0008  * For the list of contributors see $ROOTSYS/README/CREDITS.             *
0009  *************************************************************************/
0010 
0011 #ifndef ROOT_RDF_TINTERFACE
0012 #define ROOT_RDF_TINTERFACE
0013 
0014 #include "ROOT/RDataSource.hxx"
0015 #include "ROOT/RDF/ActionHelpers.hxx"
0016 #include "ROOT/RDF/HistoModels.hxx"
0017 #include "ROOT/RDF/InterfaceUtils.hxx"
0018 #include "ROOT/RDF/RColumnRegister.hxx"
0019 #include "ROOT/RDF/RDefaultValueFor.hxx"
0020 #include "ROOT/RDF/RDefine.hxx"
0021 #include "ROOT/RDF/RDefinePerSample.hxx"
0022 #include "ROOT/RDF/RFilter.hxx"
0023 #include "ROOT/RDF/RInterfaceBase.hxx"
0024 #include "ROOT/RDF/RVariation.hxx"
0025 #include "ROOT/RDF/RLazyDSImpl.hxx"
0026 #include "ROOT/RDF/RLoopManager.hxx"
0027 #include "ROOT/RDF/RRange.hxx"
0028 #include "ROOT/RDF/RFilterWithMissingValues.hxx"
0029 #include "ROOT/RDF/Utils.hxx"
0030 #include "ROOT/RDF/RDFDescription.hxx"
0031 #include "ROOT/RDF/RVariationsDescription.hxx"
0032 #include "ROOT/RResultPtr.hxx"
0033 #include "ROOT/RSnapshotOptions.hxx"
0034 #include <string_view>
0035 #include "ROOT/RVec.hxx"
0036 #include "ROOT/TypeTraits.hxx"
0037 #include "RtypesCore.h" // for ULong64_t
0038 #include "TDirectory.h"
0039 #include "TH1.h" // For Histo actions
0040 #include "TH2.h" // For Histo actions
0041 #include "TH3.h" // For Histo actions
0042 #include "THn.h"
0043 #include "THnSparse.h"
0044 #include "TProfile.h"
0045 #include "TProfile2D.h"
0046 #include "TStatistic.h"
0047 
0048 #include "RConfigure.h" // for R__HAS_ROOT7
0049 #ifdef R__HAS_ROOT7
0050 #include <ROOT/RBinWithError.hxx>
0051 #include <ROOT/RHist.hxx>
0052 #include <ROOT/RHistEngine.hxx>
0053 #endif
0054 
0055 #include <algorithm>
0056 #include <cstddef>
0057 #include <initializer_list>
0058 #include <iterator> // std::back_insterter
0059 #include <limits>
0060 #include <memory>
0061 #include <set>
0062 #include <sstream>
0063 #include <stdexcept>
0064 #include <string>
0065 #include <type_traits> // is_same, enable_if
0066 #include <typeinfo>
0067 #include <unordered_set>
0068 #include <utility> // std::index_sequence
0069 #include <vector>
0070 #include <any>
0071 
0072 class TGraph;
0073 
0074 // Windows requires a forward decl of printValue to accept it as a valid friend function in RInterface
0075 namespace ROOT {
0076 void DisableImplicitMT();
0077 bool IsImplicitMTEnabled();
0078 void EnableImplicitMT(UInt_t numthreads);
0079 class RDataFrame;
0080 } // namespace ROOT
0081 namespace cling {
0082 std::string printValue(ROOT::RDataFrame *tdf);
0083 }
0084 
0085 namespace ROOT {
0086 namespace RDF {
0087 namespace RDFDetail = ROOT::Detail::RDF;
0088 namespace RDFInternal = ROOT::Internal::RDF;
0089 namespace TTraits = ROOT::TypeTraits;
0090 
0091 template <typename Proxied>
0092 class RInterface;
0093 
0094 using RNode = RInterface<::ROOT::Detail::RDF::RNodeBase>;
0095 } // namespace RDF
0096 
0097 namespace Internal {
0098 namespace RDF {
0099 class GraphCreatorHelper;
0100 void ChangeEmptyEntryRange(const ROOT::RDF::RNode &node, std::pair<ULong64_t, ULong64_t> &&newRange);
0101 void ChangeBeginAndEndEntries(const RNode &node, Long64_t begin, Long64_t end);
0102 void ChangeSpec(const ROOT::RDF::RNode &node, ROOT::RDF::Experimental::RDatasetSpec &&spec);
0103 std::vector<std::pair<std::uint64_t, std::uint64_t>> GetDatasetGlobalClusterBoundaries(const RNode &node);
0104 void TriggerRun(ROOT::RDF::RNode node);
0105 std::string GetDataSourceLabel(const ROOT::RDF::RNode &node);
0106 void SetTTreeLifeline(ROOT::RDF::RNode &node, std::any lifeline);
0107 } // namespace RDF
0108 } // namespace Internal
0109 
0110 namespace RDF {
0111 
0112 // clang-format off
0113 /**
0114  * \class ROOT::RDF::RInterface
0115  * \ingroup dataframe
0116  * \brief The public interface to the RDataFrame federation of classes.
0117  * \tparam Proxied One of the "node" base types (e.g. RLoopManager, RFilterBase). The user never specifies this type manually.
0118  *
0119  * The documentation of each method features a one liner illustrating how to use the method, for example showing how
0120  * the majority of the template parameters are automatically deduced requiring no or very little effort by the user.
0121  */
0122 // clang-format on
0123 template <typename Proxied>
0124 class RInterface : public RInterfaceBase {
0125    using RFilterBase = RDFDetail::RFilterBase;
0126    using RRangeBase = RDFDetail::RRangeBase;
0127    using RLoopManager = RDFDetail::RLoopManager;
0128    friend std::string cling::printValue(::ROOT::RDataFrame *tdf); // For a nice printing at the prompt
0129    friend class RDFInternal::GraphDrawing::GraphCreatorHelper;
0130 
0131    template <typename T>
0132    friend class RInterface;
0133 
0134    friend void RDFInternal::TriggerRun(RNode node);
0135    friend void RDFInternal::ChangeEmptyEntryRange(const RNode &node, std::pair<ULong64_t, ULong64_t> &&newRange);
0136    friend void RDFInternal::ChangeBeginAndEndEntries(const RNode &node, Long64_t start, Long64_t end);
0137    friend void RDFInternal::ChangeSpec(const RNode &node, ROOT::RDF::Experimental::RDatasetSpec &&spec);
0138    friend std::vector<std::pair<std::uint64_t, std::uint64_t>>
0139    RDFInternal::GetDatasetGlobalClusterBoundaries(const RNode &node);
0140    friend std::string ROOT::Internal::RDF::GetDataSourceLabel(const RNode &node);
0141    friend void ROOT::Internal::RDF::SetTTreeLifeline(ROOT::RDF::RNode &node, std::any lifeline);
0142    std::shared_ptr<Proxied> fProxiedPtr; ///< Smart pointer to the graph node encapsulated by this RInterface.
0143 
0144 public:
0145    ////////////////////////////////////////////////////////////////////////////
0146    /// \brief Copy-assignment operator for RInterface.
0147    RInterface &operator=(const RInterface &) = default;
0148 
0149    ////////////////////////////////////////////////////////////////////////////
0150    /// \brief Copy-ctor for RInterface.
0151    RInterface(const RInterface &) = default;
0152 
0153    ////////////////////////////////////////////////////////////////////////////
0154    /// \brief Move-ctor for RInterface.
0155    RInterface(RInterface &&) = default;
0156 
0157    ////////////////////////////////////////////////////////////////////////////
0158    /// \brief Move-assignment operator for RInterface.
0159    RInterface &operator=(RInterface &&) = default;
0160 
0161    ////////////////////////////////////////////////////////////////////////////
0162    /// \brief Build a RInterface from a RLoopManager.
0163    /// This constructor is only available for RInterface<RLoopManager>.
0164    template <typename T = Proxied, typename = std::enable_if_t<std::is_same<T, RLoopManager>::value, int>>
0165    RInterface(const std::shared_ptr<RLoopManager> &proxied) : RInterfaceBase(proxied), fProxiedPtr(proxied)
0166    {
0167    }
0168 
0169    ////////////////////////////////////////////////////////////////////////////
0170    /// \brief Cast any RDataFrame node to a common type ROOT::RDF::RNode.
0171    /// Different RDataFrame methods return different C++ types. All nodes, however,
0172    /// can be cast to this common type at the cost of a small performance penalty.
0173    /// This allows, for example, storing RDataFrame nodes in a vector, or passing them
0174    /// around via (non-template, C++11) helper functions.
0175    /// Example usage:
0176    /// ~~~{.cpp}
0177    /// // a function that conditionally adds a Range to a RDataFrame node.
0178    /// RNode MaybeAddRange(RNode df, bool mustAddRange)
0179    /// {
0180    ///    return mustAddRange ? df.Range(1) : df;
0181    /// }
0182    /// // use as :
0183    /// ROOT::RDataFrame df(10);
0184    /// auto maybeRanged = MaybeAddRange(df, true);
0185    /// ~~~
0186    /// Note that it is not a problem to pass RNode's by value.
0187    operator RNode() const
0188    {
0189       return RNode(std::static_pointer_cast<::ROOT::Detail::RDF::RNodeBase>(fProxiedPtr), *fLoopManager, fColRegister);
0190    }
0191 
0192    ////////////////////////////////////////////////////////////////////////////
0193    /// \brief Append a filter to the call graph.
0194    /// \param[in] f Function, lambda expression, functor class or any other callable object. It must return a `bool`
0195    /// signalling whether the event has passed the selection (true) or not (false).
0196    /// \param[in] columns Names of the columns/branches in input to the filter function.
0197    /// \param[in] name Optional name of this filter. See `Report`.
0198    /// \return the filter node of the computation graph.
0199    ///
0200    /// Append a filter node at the point of the call graph corresponding to the
0201    /// object this method is called on.
0202    /// The callable `f` should not have side-effects (e.g. modification of an
0203    /// external or static variable) to ensure correct results when implicit
0204    /// multi-threading is active.
0205    ///
0206    /// RDataFrame only evaluates filters when necessary: if multiple filters
0207    /// are chained one after another, they are executed in order and the first
0208    /// one returning false causes the event to be discarded.
0209    /// Even if multiple actions or transformations depend on the same filter,
0210    /// it is executed once per entry. If its result is requested more than
0211    /// once, the cached result is served.
0212    ///
0213    /// ### Example usage:
0214    /// ~~~{.cpp}
0215    /// // C++ callable (function, functor class, lambda...) that takes two parameters of the types of "x" and "y"
0216    /// auto filtered = df.Filter(myCut, {"x", "y"});
0217    ///
0218    /// // String: it must contain valid C++ except that column names can be used instead of variable names
0219    /// auto filtered = df.Filter("x*y > 0");
0220    /// ~~~
0221    ///
0222    /// \note If the body of the string expression contains an explicit `return` statement (even if it is in a nested
0223    /// scope), RDataFrame _will not_ add another one in front of the expression. So this will not work:
0224    /// ~~~{.cpp}
0225    /// df.Filter("Sum(Map(vec, [](float e) { return e*e > 0.5; }))")
0226    /// ~~~
0227    /// but instead this will:
0228    /// ~~~{.cpp}
0229    /// df.Filter("return Sum(Map(vec, [](float e) { return e*e > 0.5; }))")
0230    /// ~~~
0231    template <typename F, std::enable_if_t<!std::is_convertible<F, std::string>::value, int> = 0>
0232    RInterface<RDFDetail::RFilter<F, Proxied>> Filter(F f, const ColumnNames_t &columns = {}, std::string_view name = "")
0233    {
0234       RDFInternal::CheckFilter(f);
0235       using ColTypes_t = typename TTraits::CallableTraits<F>::arg_types;
0236       constexpr auto nColumns = ColTypes_t::list_size;
0237       const auto validColumnNames = GetValidatedColumnNames(nColumns, columns);
0238       CheckAndFillDSColumns(validColumnNames, ColTypes_t());
0239 
0240       using F_t = RDFDetail::RFilter<F, Proxied>;
0241 
0242       auto filterPtr = std::make_shared<F_t>(std::move(f), validColumnNames, fProxiedPtr, fColRegister, name);
0243       return RInterface<F_t>(std::move(filterPtr), *fLoopManager, fColRegister);
0244    }
0245 
0246    ////////////////////////////////////////////////////////////////////////////
0247    /// \brief Append a filter to the call graph.
0248    /// \param[in] f Function, lambda expression, functor class or any other callable object. It must return a `bool`
0249    /// signalling whether the event has passed the selection (true) or not (false).
0250    /// \param[in] name Optional name of this filter. See `Report`.
0251    /// \return the filter node of the computation graph.
0252    ///
0253    /// Refer to the first overload of this method for the full documentation.
0254    template <typename F, std::enable_if_t<!std::is_convertible<F, std::string>::value, int> = 0>
0255    RInterface<RDFDetail::RFilter<F, Proxied>> Filter(F f, std::string_view name)
0256    {
0257       // The sfinae is there in order to pick up the overloaded method which accepts two strings
0258       // rather than this template method.
0259       return Filter(f, {}, name);
0260    }
0261 
0262    ////////////////////////////////////////////////////////////////////////////
0263    /// \brief Append a filter to the call graph.
0264    /// \param[in] f Function, lambda expression, functor class or any other callable object. It must return a `bool`
0265    /// signalling whether the event has passed the selection (true) or not (false).
0266    /// \param[in] columns Names of the columns/branches in input to the filter function.
0267    /// \return the filter node of the computation graph.
0268    ///
0269    /// Refer to the first overload of this method for the full documentation.
0270    template <typename F>
0271    RInterface<RDFDetail::RFilter<F, Proxied>> Filter(F f, const std::initializer_list<std::string> &columns)
0272    {
0273       return Filter(f, ColumnNames_t{columns});
0274    }
0275 
0276    ////////////////////////////////////////////////////////////////////////////
0277    /// \brief Append a filter to the call graph.
0278    /// \param[in] expression The filter expression in C++
0279    /// \param[in] name Optional name of this filter. See `Report`.
0280    /// \return the filter node of the computation graph.
0281    ///
0282    /// The expression is just-in-time compiled and used to filter entries. It must
0283    /// be valid C++ syntax in which variable names are substituted with the names
0284    /// of branches/columns.
0285    ///
0286    /// ### Example usage:
0287    /// ~~~{.cpp}
0288    /// auto filtered_df = df.Filter("myCollection.size() > 3");
0289    /// auto filtered_name_df = df.Filter("myCollection.size() > 3", "Minumum collection size");
0290    /// ~~~
0291    ///
0292    /// \note If the body of the string expression contains an explicit `return` statement (even if it is in a nested
0293    /// scope), RDataFrame _will not_ add another one in front of the expression. So this will not work:
0294    /// ~~~{.cpp}
0295    /// df.Filter("Sum(Map(vec, [](float e) { return e*e > 0.5; }))")
0296    /// ~~~
0297    /// but instead this will:
0298    /// ~~~{.cpp}
0299    /// df.Filter("return Sum(Map(vec, [](float e) { return e*e > 0.5; }))")
0300    /// ~~~
0301    RInterface<RDFDetail::RJittedFilter> Filter(std::string_view expression, std::string_view name = "")
0302    {
0303       const auto jittedFilter = RDFInternal::BookFilterJit(RDFInternal::UpcastNode(fProxiedPtr), name, expression,
0304                                                            fColRegister, nullptr, GetDataSource());
0305 
0306       return RInterface<RDFDetail::RJittedFilter>(std::move(jittedFilter), *fLoopManager, fColRegister);
0307    }
0308 
0309    ////////////////////////////////////////////////////////////////////////////
0310    /// \brief Discard entries with missing values
0311    /// \param[in] column Column name whose entries with missing values should be discarded
0312    /// \return The filter node of the computation graph
0313    ///
0314    /// This operation is useful in case an entry of the dataset is incomplete,
0315    /// i.e. if one or more of the columns do not have valid values. If the value
0316    /// of the input column is missing for an entry, the entire entry will be
0317    /// discarded from the rest of this branch of the computation graph.
0318    ///
0319    /// Use cases include:
0320    /// * When processing multiple files, one or more of them is missing a column
0321    /// * In horizontal joining with entry matching, a certain dataset has no
0322    ///   match for the current entry.
0323    ///
0324    /// ### Example usage:
0325    ///
0326    /// \code{.py}
0327    /// # Assume a dataset with columns [idx, x] matching another dataset with
0328    /// # columns [idx, y]. For idx == 42, the right-hand dataset has no match
0329    /// df = ROOT.RDataFrame(dataset)
0330    /// df_nomissing = df.FilterAvailable("idx").Define("z", "x + y")
0331    /// colz = df_nomissing.Take[int]("z")
0332    /// \endcode
0333    ///
0334    /// \code{.cpp}
0335    /// // Assume a dataset with columns [idx, x] matching another dataset with
0336    /// // columns [idx, y]. For idx == 42, the right-hand dataset has no match
0337    /// ROOT::RDataFrame df{dataset};
0338    /// auto df_nomissing = df.FilterAvailable("idx")
0339    ///                       .Define("z", [](int x, int y) { return x + y; }, {"x", "y"});
0340    /// auto colz = df_nomissing.Take<int>("z");
0341    /// \endcode
0342    ///
0343    /// \note See FilterMissing() if you want to keep only the entries with
0344    ///       missing values instead.
0345    RInterface<RDFDetail::RFilterWithMissingValues<Proxied>> FilterAvailable(std::string_view column)
0346    {
0347       const auto columns = ColumnNames_t{column.data()};
0348       // For now disable this functionality in case of an empty data source and
0349       // the column name was not defined previously.
0350       if (ROOT::Internal::RDF::GetDataSourceLabel(*this) == "EmptyDS")
0351          throw std::runtime_error("Unknown column: \"" + std::string(column) + "\"");
0352       using F_t = RDFDetail::RFilterWithMissingValues<Proxied>;
0353       auto filterPtr = std::make_shared<F_t>(/*discardEntry*/ true, fProxiedPtr, fColRegister, columns);
0354       CheckAndFillDSColumns(columns, TTraits::TypeList<void>{});
0355       return RInterface<F_t>(std::move(filterPtr), *fLoopManager, fColRegister);
0356    }
0357 
0358    ////////////////////////////////////////////////////////////////////////////
0359    /// \brief Keep only the entries that have missing values.
0360    /// \param[in] column Column name whose entries with missing values should be kept
0361    /// \return The filter node of the computation graph
0362    ///
0363    /// This operation is useful in case an entry of the dataset is incomplete,
0364    /// i.e. if one or more of the columns do not have valid values. It only
0365    /// keeps the entries for which the value of the input column is missing.
0366    ///
0367    /// Use cases include:
0368    /// * When processing multiple files, one or more of them is missing a column
0369    /// * In horizontal joining with entry matching, a certain dataset has no
0370    ///   match for the current entry.
0371    ///
0372    /// ### Example usage:
0373    ///
0374    /// \code{.py}
0375    /// # Assume a dataset made of two files vertically chained together, one has
0376    /// # column "x" and the other has column "y"
0377    /// df = ROOT.RDataFrame(dataset)
0378    /// df_valid_col_x = df.FilterMissing("y")
0379    /// df_valid_col_y = df.FilterMissing("x")
0380    /// display_x = df_valid_col_x.Display(("x",))
0381    /// display_y = df_valid_col_y.Display(("y",))
0382    /// \endcode
0383    ///
0384    /// \code{.cpp}
0385    /// // Assume a dataset made of two files vertically chained together, one has
0386    /// // column "x" and the other has column "y"
0387    /// ROOT.RDataFrame df{dataset};
0388    /// auto df_valid_col_x = df.FilterMissing("y");
0389    /// auto df_valid_col_y = df.FilterMissing("x");
0390    /// auto display_x = df_valid_col_x.Display<int>({"x"});
0391    /// auto display_y = df_valid_col_y.Display<int>({"y"});
0392    /// \endcode
0393    ///
0394    /// \note See FilterAvailable() if you want to discard the entries in case
0395    ///       there is a missing value instead.
0396    RInterface<RDFDetail::RFilterWithMissingValues<Proxied>> FilterMissing(std::string_view column)
0397    {
0398       const auto columns = ColumnNames_t{column.data()};
0399       // For now disable this functionality in case of an empty data source and
0400       // the column name was not defined previously.
0401       if (ROOT::Internal::RDF::GetDataSourceLabel(*this) == "EmptyDS")
0402          throw std::runtime_error("Unknown column: \"" + std::string(column) + "\"");
0403       using F_t = RDFDetail::RFilterWithMissingValues<Proxied>;
0404       auto filterPtr = std::make_shared<F_t>(/*discardEntry*/ false, fProxiedPtr, fColRegister, columns);
0405       CheckAndFillDSColumns(columns, TTraits::TypeList<void>{});
0406       return RInterface<F_t>(std::move(filterPtr), *fLoopManager, fColRegister);
0407    }
0408 
0409    // clang-format off
0410    ////////////////////////////////////////////////////////////////////////////
0411    /// \brief Define a new column.
0412    /// \param[in] name The name of the defined column.
0413    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column. This callable must be thread safe when used with multiple threads.
0414    /// \param[in] columns Names of the columns/branches in input to the producer function.
0415    /// \return the first node of the computation graph for which the new quantity is defined.
0416    ///
0417    /// Define a column that will be visible from all subsequent nodes
0418    /// of the functional chain. The `expression` is only evaluated for entries that pass
0419    /// all the preceding filters.
0420    /// A new variable is created called `name`, accessible as if it was contained
0421    /// in the dataset from subsequent transformations/actions.
0422    ///
0423    /// Use cases include:
0424    /// * caching the results of complex calculations for easy and efficient multiple access
0425    /// * extraction of quantities of interest from complex objects
0426    ///
0427    /// An exception is thrown if the name of the new column is already in use in this branch of the computation graph.
0428    /// Note that the callable must be thread safe when called from multiple threads. Use DefineSlot() if needed.
0429    ///
0430    /// ### Example usage:
0431    /// ~~~{.cpp}
0432    /// // assuming a function with signature:
0433    /// double myComplexCalculation(const RVec<float> &muon_pts);
0434    /// // we can pass it directly to Define
0435    /// auto df_with_define = df.Define("newColumn", myComplexCalculation, {"muon_pts"});
0436    /// // alternatively, we can pass the body of the function as a string, as in Filter:
0437    /// auto df_with_define = df.Define("newColumn", "x*x + y*y");
0438    /// ~~~
0439    ///
0440    /// \note If the body of the string expression contains an explicit `return` statement (even if it is in a nested
0441    /// scope), RDataFrame _will not_ add another one in front of the expression. So this will not work:
0442    /// ~~~{.cpp}
0443    /// df.Define("x2", "Map(v, [](float e) { return e*e; })")
0444    /// ~~~
0445    /// but instead this will:
0446    /// ~~~{.cpp}
0447    /// df.Define("x2", "return Map(v, [](float e) { return e*e; })")
0448    /// ~~~
0449    template <typename F, typename std::enable_if_t<!std::is_convertible<F, std::string>::value, int> = 0>
0450    RInterface<Proxied> Define(std::string_view name, F expression, const ColumnNames_t &columns = {})
0451    {
0452       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::None>(name, std::move(expression), columns, "Define");
0453    }
0454    // clang-format on
0455 
0456    // clang-format off
0457    ////////////////////////////////////////////////////////////////////////////
0458    /// \brief Define a new column with a value dependent on the processing slot.
0459    /// \param[in] name The name of the defined column.
0460    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column.
0461    /// \param[in] columns Names of the columns/branches in input to the producer function (excluding the slot number).
0462    /// \return the first node of the computation graph for which the new quantity is defined.
0463    ///
0464    /// This alternative implementation of `Define` is meant as a helper to evaluate new column values in a thread-safe manner.
0465    /// The expression must be a callable of signature R(unsigned int, T1, T2, ...) where `T1, T2...` are the types
0466    /// of the columns that the expression takes as input. The first parameter is reserved for an unsigned integer
0467    /// representing a "slot number". RDataFrame guarantees that different threads will invoke the expression with
0468    /// different slot numbers - slot numbers will range from zero to ROOT::GetThreadPoolSize()-1.
0469    /// Note that there is no guarantee as to how often each slot will be reached during the event loop.
0470    ///
0471    /// The following two calls are equivalent, although `DefineSlot` is slightly more performant:
0472    /// ~~~{.cpp}
0473    /// int function(unsigned int, double, double);
0474    /// df.Define("x", function, {"rdfslot_", "column1", "column2"})
0475    /// df.DefineSlot("x", function, {"column1", "column2"})
0476    /// ~~~
0477    ///
0478    /// See Define() for more information.
0479    template <typename F>
0480    RInterface<Proxied> DefineSlot(std::string_view name, F expression, const ColumnNames_t &columns = {})
0481    {
0482       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::Slot>(name, std::move(expression), columns, "DefineSlot");
0483    }
0484    // clang-format on
0485 
0486    // clang-format off
0487    ////////////////////////////////////////////////////////////////////////////
0488    /// \brief Define a new column with a value dependent on the processing slot and the current entry.
0489    /// \param[in] name The name of the defined column.
0490    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column.
0491    /// \param[in] columns Names of the columns/branches in input to the producer function (excluding slot and entry).
0492    /// \return the first node of the computation graph for which the new quantity is defined.
0493    ///
0494    /// This alternative implementation of `Define` is meant as a helper in writing entry-specific, thread-safe custom
0495    /// columns. The expression must be a callable of signature R(unsigned int, ULong64_t, T1, T2, ...) where `T1, T2...`
0496    /// are the types of the columns that the expression takes as input. The first parameter is reserved for an unsigned
0497    /// integer representing a "slot number". RDataFrame guarantees that different threads will invoke the expression with
0498    /// different slot numbers - slot numbers will range from zero to ROOT::GetThreadPoolSize()-1.
0499    /// Note that there is no guarantee as to how often each slot will be reached during the event loop.
0500    /// The second parameter is reserved for a `ULong64_t` representing the current entry being processed by the current thread.
0501    ///
0502    /// The following two `Define`s are equivalent, although `DefineSlotEntry` is slightly more performant:
0503    /// ~~~{.cpp}
0504    /// int function(unsigned int, ULong64_t, double, double);
0505    /// Define("x", function, {"rdfslot_", "rdfentry_", "column1", "column2"})
0506    /// DefineSlotEntry("x", function, {"column1", "column2"})
0507    /// ~~~
0508    ///
0509    /// See Define() for more information.
0510    template <typename F>
0511    RInterface<Proxied> DefineSlotEntry(std::string_view name, F expression, const ColumnNames_t &columns = {})
0512    {
0513       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::SlotAndEntry>(name, std::move(expression), columns,
0514                                                                         "DefineSlotEntry");
0515    }
0516    // clang-format on
0517 
0518    ////////////////////////////////////////////////////////////////////////////
0519    /// \brief Define a new column.
0520    /// \param[in] name The name of the defined column.
0521    /// \param[in] expression An expression in C++ which represents the defined value
0522    /// \return the first node of the computation graph for which the new quantity is defined.
0523    ///
0524    /// The expression is just-in-time compiled and used to produce the column entries.
0525    /// It must be valid C++ syntax in which variable names are substituted with the names
0526    /// of branches/columns.
0527    ///
0528    /// \note If the body of the string expression contains an explicit `return` statement (even if it is in a nested
0529    /// scope), RDataFrame _will not_ add another one in front of the expression. So this will not work:
0530    /// ~~~{.cpp}
0531    /// df.Define("x2", "Map(v, [](float e) { return e*e; })")
0532    /// ~~~
0533    /// but instead this will:
0534    /// ~~~{.cpp}
0535    /// df.Define("x2", "return Map(v, [](float e) { return e*e; })")
0536    /// ~~~
0537    ///
0538    /// Refer to the first overload of this method for the full documentation.
0539    RInterface<Proxied> Define(std::string_view name, std::string_view expression)
0540    {
0541       constexpr auto where = "Define";
0542       RDFInternal::CheckValidCppVarName(name, where);
0543       // these checks must be done before jitting lest we throw exceptions in jitted code
0544       RDFInternal::CheckForRedefinition(where, name, fColRegister,
0545                                         GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
0546 
0547       auto jittedDefine = RDFInternal::BookDefineJit(name, expression, *fLoopManager, GetDataSource(), fColRegister);
0548 
0549       RDFInternal::RColumnRegister newCols(fColRegister);
0550       newCols.AddDefine(std::move(jittedDefine));
0551 
0552       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
0553 
0554       return newInterface;
0555    }
0556 
0557    ////////////////////////////////////////////////////////////////////////////
0558    /// \brief Overwrite the value and/or type of an existing column.
0559    /// \param[in] name The name of the column to redefine.
0560    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column.
0561    /// \param[in] columns Names of the columns/branches in input to the expression.
0562    /// \return the first node of the computation graph for which the quantity is redefined.
0563    ///
0564    /// The old value of the column can be used as an input for the expression.
0565    ///
0566    /// An exception is thrown in case the column to redefine does not already exist.
0567    /// See Define() for more information.
0568    template <typename F, std::enable_if_t<!std::is_convertible<F, std::string>::value, int> = 0>
0569    RInterface<Proxied> Redefine(std::string_view name, F expression, const ColumnNames_t &columns = {})
0570    {
0571       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::None>(name, std::move(expression), columns, "Redefine");
0572    }
0573 
0574    // clang-format off
0575    ////////////////////////////////////////////////////////////////////////////
0576    /// \brief Overwrite the value and/or type of an existing column.
0577    /// \param[in] name The name of the column to redefine.
0578    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column.
0579    /// \param[in] columns Names of the columns/branches in input to the producer function (excluding slot).
0580    /// \return the first node of the computation graph for which the new quantity is defined.
0581    ///
0582    /// The old value of the column can be used as an input for the expression.
0583    /// An exception is thrown in case the column to redefine does not already exist.
0584    ///
0585    /// See DefineSlot() for more information.
0586    // clang-format on
0587    template <typename F>
0588    RInterface<Proxied> RedefineSlot(std::string_view name, F expression, const ColumnNames_t &columns = {})
0589    {
0590       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::Slot>(name, std::move(expression), columns, "RedefineSlot");
0591    }
0592 
0593    // clang-format off
0594    ////////////////////////////////////////////////////////////////////////////
0595    /// \brief Overwrite the value and/or type of an existing column.
0596    /// \param[in] name The name of the column to redefine.
0597    /// \param[in] expression Function, lambda expression, functor class or any other callable object producing the defined value. Returns the value that will be assigned to the defined column.
0598    /// \param[in] columns Names of the columns/branches in input to the producer function (excluding slot and entry).
0599    /// \return the first node of the computation graph for which the new quantity is defined.
0600    ///
0601    /// The old value of the column can be used as an input for the expression.
0602    /// An exception is thrown in case the column to re-define does not already exist.
0603    ///
0604    /// See DefineSlotEntry() for more information.
0605    // clang-format on
0606    template <typename F>
0607    RInterface<Proxied> RedefineSlotEntry(std::string_view name, F expression, const ColumnNames_t &columns = {})
0608    {
0609       return DefineImpl<F, RDFDetail::ExtraArgsForDefine::SlotAndEntry>(name, std::move(expression), columns,
0610                                                                         "RedefineSlotEntry");
0611    }
0612 
0613    ////////////////////////////////////////////////////////////////////////////
0614    /// \brief Overwrite the value and/or type of an existing column.
0615    /// \param[in] name The name of the column to redefine.
0616    /// \param[in] expression An expression in C++ which represents the defined value
0617    /// \return the first node of the computation graph for which the new quantity is defined.
0618    ///
0619    /// The expression is just-in-time compiled and used to produce the column entries.
0620    /// It must be valid C++ syntax in which variable names are substituted with the names
0621    /// of branches/columns.
0622    ///
0623    /// The old value of the column can be used as an input for the expression.
0624    /// An exception is thrown in case the column to re-define does not already exist.
0625    ///
0626    /// Aliases cannot be overridden. See the corresponding Define() overload for more information.
0627    RInterface<Proxied> Redefine(std::string_view name, std::string_view expression)
0628    {
0629       constexpr auto where = "Redefine";
0630       RDFInternal::CheckValidCppVarName(name, where);
0631       RDFInternal::CheckForDefinition(where, name, fColRegister,
0632                                       GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
0633       RDFInternal::CheckForNoVariations(where, name, fColRegister);
0634 
0635       auto jittedDefine = RDFInternal::BookDefineJit(name, expression, *fLoopManager, GetDataSource(), fColRegister);
0636 
0637       RDFInternal::RColumnRegister newCols(fColRegister);
0638       newCols.AddDefine(std::move(jittedDefine));
0639 
0640       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
0641 
0642       return newInterface;
0643    }
0644 
0645    ////////////////////////////////////////////////////////////////////////////
0646    /// \brief In case the value in the given column is missing, provide a default value
0647    /// \tparam T The type of the column
0648    /// \param[in] column Column name where missing values should be replaced by the given default value
0649    /// \param[in] defaultValue Value to provide instead of a missing value
0650    /// \return The node of the graph that will provide a default value
0651    ///
0652    /// This operation is useful in case an entry of the dataset is incomplete,
0653    /// i.e. if one or more of the columns do not have valid values. It does not
0654    /// modify the values of the column, but in case any entry is missing, it
0655    /// will provide the default value to downstream nodes instead.
0656    ///
0657    /// Use cases include:
0658    /// * When processing multiple files, one or more of them is missing a column
0659    /// * In horizontal joining with entry matching, a certain dataset has no
0660    ///   match for the current entry.
0661    ///
0662    /// ### Example usage:
0663    ///
0664    /// \code{.cpp}
0665    /// // Assume a dataset with columns [idx, x] matching another dataset with
0666    /// // columns [idx, y]. For idx == 42, the right-hand dataset has no match
0667    /// ROOT::RDataFrame df{dataset};
0668    /// auto df_default = df.DefaultValueFor("y", 33)
0669    ///                     .Define("z", [](int x, int y) { return x + y; }, {"x", "y"});
0670    /// auto colz = df_default.Take<int>("z");
0671    /// \endcode
0672    ///
0673    /// \code{.py}
0674    /// df = ROOT.RDataFrame(dataset)
0675    /// df_default = df.DefaultValueFor("y", 33).Define("z", "x + y")
0676    /// colz = df_default.Take[int]("z")
0677    /// \endcode
0678    template <typename T>
0679    RInterface<Proxied> DefaultValueFor(std::string_view column, const T &defaultValue)
0680    {
0681       constexpr auto where{"DefaultValueFor"};
0682       RDFInternal::CheckForNoVariations(where, column, fColRegister);
0683       // For now disable this functionality in case of an empty data source and
0684       // the column name was not defined previously.
0685       if (ROOT::Internal::RDF::GetDataSourceLabel(*this) == "EmptyDS")
0686          RDFInternal::CheckForDefinition(where, column, fColRegister,
0687                                          GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
0688 
0689       // Declare return type to the interpreter, for future use by jitted actions
0690       auto retTypeName = RDFInternal::TypeID2TypeName(typeid(T));
0691       if (retTypeName.empty()) {
0692          // The type is not known to the interpreter.
0693          // We must not error out here, but if/when this column is used in jitted code
0694          const auto demangledType = RDFInternal::DemangleTypeIdName(typeid(T));
0695          retTypeName = "CLING_UNKNOWN_TYPE_" + demangledType;
0696       }
0697 
0698       const auto validColumnNames = ColumnNames_t{column.data()};
0699       auto newColumn = std::make_shared<ROOT::Internal::RDF::RDefaultValueFor<T>>(
0700          column, retTypeName, defaultValue, validColumnNames, fColRegister, *fLoopManager);
0701       CheckAndFillDSColumns(validColumnNames, TTraits::TypeList<T>{});
0702 
0703       RDFInternal::RColumnRegister newCols(fColRegister);
0704       newCols.AddDefine(std::move(newColumn));
0705 
0706       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
0707 
0708       return newInterface;
0709    }
0710 
0711    // clang-format off
0712    ////////////////////////////////////////////////////////////////////////////
0713    /// \brief Define a new column that is updated when the input sample changes.
0714    /// \param[in] name The name of the defined column.
0715    /// \param[in] expression A C++ callable that computes the new value of the defined column.
0716    /// \return the first node of the computation graph for which the new quantity is defined.
0717    ///
0718    /// The signature of the callable passed as second argument should be `T(unsigned int slot, const ROOT::RDF::RSampleInfo &id)`
0719    /// where:
0720    /// - `T` is the type of the defined column
0721    /// - `slot` is a number in the range [0, nThreads) that is different for each processing thread. This can simplify
0722    ///   the definition of thread-safe callables if you are interested in using parallel capabilities of RDataFrame.
0723    /// - `id` is an instance of a ROOT::RDF::RSampleInfo object which contains information about the sample which is
0724    ///   being processed (see the class docs for more information).
0725    ///
0726    /// DefinePerSample() is useful to e.g. define a quantity that depends on which TTree in which TFile is being
0727    /// processed or to inject a callback into the event loop that is only called when the processing of a new sample
0728    /// starts rather than at every entry.
0729    ///
0730    /// The callable will be invoked once per input TTree or once per multi-thread task, whichever is more often.
0731    ///
0732    /// ### Example usage:
0733    /// ~~~{.cpp}
0734    /// ROOT::RDataFrame df{"mytree", {"sample1.root","sample2.root"}};
0735    /// df.DefinePerSample("weightbysample",
0736    ///                    [](unsigned int slot, const ROOT::RDF::RSampleInfo &id)
0737    ///                    { return id.Contains("sample1") ? 1.0f : 2.0f; });
0738    /// ~~~
0739    // clang-format on
0740    // TODO we could SFINAE on F's signature to provide friendlier compilation errors in case of signature mismatch
0741    template <typename F, typename RetType_t = typename TTraits::CallableTraits<F>::ret_type>
0742    RInterface<Proxied> DefinePerSample(std::string_view name, F expression)
0743    {
0744       RDFInternal::CheckValidCppVarName(name, "DefinePerSample");
0745       RDFInternal::CheckForRedefinition("DefinePerSample", name, fColRegister,
0746                                         GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
0747 
0748       auto retTypeName = RDFInternal::TypeID2TypeName(typeid(RetType_t));
0749       if (retTypeName.empty()) {
0750          // The type is not known to the interpreter.
0751          // We must not error out here, but if/when this column is used in jitted code
0752          const auto demangledType = RDFInternal::DemangleTypeIdName(typeid(RetType_t));
0753          retTypeName = "CLING_UNKNOWN_TYPE_" + demangledType;
0754       }
0755 
0756       auto newColumn =
0757          std::make_shared<RDFDetail::RDefinePerSample<F>>(name, retTypeName, std::move(expression), *fLoopManager);
0758 
0759       RDFInternal::RColumnRegister newCols(fColRegister);
0760       newCols.AddDefine(std::move(newColumn));
0761       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
0762       return newInterface;
0763    }
0764 
0765    // clang-format off
0766    ////////////////////////////////////////////////////////////////////////////
0767    /// \brief Define a new column that is updated when the input sample changes.
0768    /// \param[in] name The name of the defined column.
0769    /// \param[in] expression A valid C++ expression as a string, which will be used to compute the defined value.
0770    /// \return the first node of the computation graph for which the new quantity is defined.
0771    ///
0772    /// The expression is just-in-time compiled and used to produce the column entries.
0773    /// It must be valid C++ syntax and the usage of the special variable names `rdfslot_` and `rdfsampleinfo_` is
0774    /// permitted, where these variables will take the same values as the `slot` and `id` parameters described at the
0775    /// DefinePerSample(std::string_view name, F expression) overload. See the documentation of that overload for more information.
0776    ///
0777    /// ### Example usage:
0778    /// ~~~{.py}
0779    /// df = ROOT.RDataFrame('mytree', ['sample1.root','sample2.root'])
0780    /// df.DefinePerSample('weightbysample', 'rdfsampleinfo_.Contains("sample1") ? 1.0f : 2.0f')
0781    /// ~~~
0782    ///
0783    /// \note
0784    /// If you have declared some C++ function to the interpreter, the correct syntax to call that function with this
0785    /// overload of DefinePerSample is by calling it explicitly with the special names `rdfslot_` and `rdfsampleinfo_` as
0786    /// input parameters. This is for example the correct way to call this overload when working in PyROOT:
0787    /// ~~~{.py}
0788    /// ROOT.gInterpreter.Declare(
0789    /// """
0790    /// float weights(unsigned int slot, const ROOT::RDF::RSampleInfo &id){
0791    ///    return id.Contains("sample1") ? 1.0f : 2.0f;
0792    /// }
0793    /// """)
0794    /// df = ROOT.RDataFrame("mytree", ["sample1.root","sample2.root"])
0795    /// df.DefinePerSample("weightsbysample", "weights(rdfslot_, rdfsampleinfo_)")
0796    /// ~~~
0797    ///
0798    /// \note
0799    /// Differently from what happens in Define(), the string expression passed to DefinePerSample cannot contain
0800    /// column names other than those mentioned above: the expression is evaluated once before the processing of the
0801    /// sample even starts, so column values are not accessible.
0802    // clang-format on
0803    RInterface<Proxied> DefinePerSample(std::string_view name, std::string_view expression)
0804    {
0805       RDFInternal::CheckValidCppVarName(name, "DefinePerSample");
0806       // these checks must be done before jitting lest we throw exceptions in jitted code
0807       RDFInternal::CheckForRedefinition("DefinePerSample", name, fColRegister,
0808                                         GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
0809 
0810       auto jittedDefine = RDFInternal::BookDefinePerSampleJit(name, expression, *fLoopManager, fColRegister);
0811 
0812       RDFInternal::RColumnRegister newCols(fColRegister);
0813       newCols.AddDefine(std::move(jittedDefine));
0814 
0815       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
0816 
0817       return newInterface;
0818    }
0819 
0820    /// \brief Register systematic variations for a single existing column using custom variation tags.
0821    /// \param[in] colName name of the column for which varied values are provided.
0822    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
0823    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
0824    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
0825    /// \param[in] inputColumns the names of the columns to be passed to the callable.
0826    /// \param[in] variationTags names for each of the varied values, e.g. `"up"` and `"down"`.
0827    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
0828    ///
0829    /// Vary provides a natural and flexible syntax to define systematic variations that automatically propagate to
0830    /// Filters, Defines and results. RDataFrame usage of columns with attached variations does not change, but for
0831    /// results that depend on any varied quantity, a map/dictionary of varied results can be produced with
0832    /// ROOT::RDF::Experimental::VariationsFor (see the example below).
0833    ///
0834    /// The dictionary will contain a "nominal" value (accessed with the "nominal" key) for the unchanged result, and
0835    /// values for each of the systematic variations that affected the result (via upstream Filters or via direct or
0836    /// indirect dependencies of the column values on some registered variations). The keys will be a composition of
0837    /// variation names and tags, e.g. "pt:up" and "pt:down" for the example below.
0838    ///
0839    /// In the following example we add up/down variations of pt and fill a histogram with a quantity that depends on pt.
0840    /// We automatically obtain three histograms in output ("nominal", "pt:up" and "pt:down"):
0841    /// ~~~{.cpp}
0842    /// auto nominal_hx =
0843    ///     df.Vary("pt", [] (double pt) { return RVecD{pt*0.9, pt*1.1}; }, {"down", "up"})
0844    ///       .Filter("pt > k")
0845    ///       .Define("x", someFunc, {"pt"})
0846    ///       .Histo1D("x");
0847    ///
0848    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
0849    /// hx["nominal"].Draw();
0850    /// hx["pt:down"].Draw("SAME");
0851    /// hx["pt:up"].Draw("SAME");
0852    /// ~~~
0853    /// RDataFrame computes all variations as part of a single loop over the data.
0854    /// In particular, this means that I/O and computation of values shared
0855    /// among variations only happen once for all variations. Thus, the event loop
0856    /// run-time typically scales much better than linearly with the number of
0857    /// variations.
0858    ///
0859    /// RDataFrame lazily computes the varied values required to produce the
0860    /// outputs of \ref ROOT::RDF::Experimental::VariationsFor "VariationsFor()". If \ref
0861    /// ROOT::RDF::Experimental::VariationsFor "VariationsFor()" was not called for a result, the computations are only
0862    /// run for the nominal case.
0863    ///
0864    /// See other overloads for examples when variations are added for multiple existing columns,
0865    /// or when the tags are auto-generated instead of being directly defined.
0866    template <typename F>
0867    RInterface<Proxied> Vary(std::string_view colName, F &&expression, const ColumnNames_t &inputColumns,
0868                             const std::vector<std::string> &variationTags, std::string_view variationName = "")
0869    {
0870       std::vector<std::string> colNames{{std::string(colName)}};
0871       const std::string theVariationName{variationName.empty() ? colName : variationName};
0872 
0873       return VaryImpl<true>(std::move(colNames), std::forward<F>(expression), inputColumns, variationTags,
0874                             theVariationName);
0875    }
0876 
0877    /// \brief Register systematic variations for a single existing column using auto-generated variation tags.
0878    /// \param[in] colName name of the column for which varied values are provided.
0879    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
0880    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
0881    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
0882    /// \param[in] inputColumns the names of the columns to be passed to the callable.
0883    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
0884    /// `"1"`, etc.
0885    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
0886    ///            colName is used if none is provided.
0887    ///
0888    /// This overload of Vary takes an nVariations parameter instead of a list of tag names.
0889    /// The varied results will be accessible via the keys of the dictionary with the form `variationName:N` where `N`
0890    /// is the corresponding sequential tag starting at 0 and going up to `nVariations - 1`.
0891    ///
0892    /// Example usage:
0893    /// ~~~{.cpp}
0894    /// auto nominal_hx =
0895    ///   df.Vary("pt", [] (double pt) { return RVecD{pt*0.9, pt*1.1}; }, 2)
0896    ///     .Histo1D("x");
0897    ///
0898    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
0899    /// hx["nominal"].Draw();
0900    /// hx["x:0"].Draw("SAME");
0901    /// hx["x:1"].Draw("SAME");
0902    /// ~~~
0903    ///
0904    /// \note See also This Vary() overload for more information.
0905    template <typename F>
0906    RInterface<Proxied> Vary(std::string_view colName, F &&expression, const ColumnNames_t &inputColumns,
0907                             std::size_t nVariations, std::string_view variationName = "")
0908    {
0909       R__ASSERT(nVariations > 0 && "Must have at least one variation.");
0910 
0911       std::vector<std::string> variationTags;
0912       variationTags.reserve(nVariations);
0913       for (std::size_t i = 0u; i < nVariations; ++i)
0914          variationTags.emplace_back(std::to_string(i));
0915 
0916       const std::string theVariationName{variationName.empty() ? colName : variationName};
0917 
0918       return Vary(colName, std::forward<F>(expression), inputColumns, std::move(variationTags), theVariationName);
0919    }
0920 
0921    /// \brief Register systematic variations for multiple existing columns using custom variation tags.
0922    /// \param[in] colNames set of names of the columns for which varied values are provided.
0923    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
0924    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
0925    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
0926    /// \param[in] inputColumns the names of the columns to be passed to the callable.
0927    /// \param[in] variationTags names for each of the varied values, e.g. `"up"` and `"down"`.
0928    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`
0929    ///
0930    /// This overload of Vary takes a list of column names as first argument and
0931    /// requires that the expression returns an RVec of RVecs of values: one inner RVec for the variations of each
0932    /// affected column. The `variationTags` are defined as `{"down", "up"}`.
0933    ///
0934    /// Example usage:
0935    /// ~~~{.cpp}
0936    /// // produce variations "ptAndEta:down" and "ptAndEta:up"
0937    /// auto nominal_hx =
0938    ///   df.Vary({"pt", "eta"}, // the columns that will vary simultaneously
0939    ///         [](double pt, double eta) { return RVec<RVecF>{{pt*0.9, pt*1.1}, {eta*0.9, eta*1.1}}; },
0940    ///         {"pt", "eta"},  // inputs to the Vary expression, independent of what columns are varied
0941    ///         {"down", "up"}, // variation tags
0942    ///         "ptAndEta")    // variation name
0943    ///     .Histo1D("pt", "eta");
0944    ///
0945    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
0946    /// hx["nominal"].Draw();
0947    /// hx["ptAndEta:down"].Draw("SAME");
0948    /// hx["ptAndEta:up"].Draw("SAME");
0949    /// ~~~
0950    ///
0951    /// \note See also This Vary() overload for more information.
0952 
0953    template <typename F>
0954    RInterface<Proxied> Vary(const std::vector<std::string> &colNames, F &&expression, const ColumnNames_t &inputColumns,
0955                             const std::vector<std::string> &variationTags, std::string_view variationName)
0956    {
0957       return VaryImpl<false>(colNames, std::forward<F>(expression), inputColumns, variationTags, variationName);
0958    }
0959 
0960    /// \brief Register systematic variations for multiple existing columns using custom variation tags.
0961    /// \param[in] colNames set of names of the columns for which varied values are provided.
0962    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
0963    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
0964    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
0965    /// \param[in] inputColumns the names of the columns to be passed to the callable.
0966    /// \param[in] variationTags names for each of the varied values, e.g. `"up"` and `"down"`.
0967    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
0968    ///            colName is used if none is provided.
0969    ///
0970    /// \note This overload ensures that the ambiguity between C++20 string, vector<string> construction from init list
0971    /// is avoided.
0972    ///
0973    /// \note See also This Vary() overload for more information.
0974    template <typename F>
0975    RInterface<Proxied>
0976    Vary(std::initializer_list<std::string> colNames, F &&expression, const ColumnNames_t &inputColumns,
0977         const std::vector<std::string> &variationTags, std::string_view variationName)
0978    {
0979       return Vary(std::vector<std::string>(colNames), std::forward<F>(expression), inputColumns, variationTags, variationName);
0980    }
0981 
0982    /// \brief Register systematic variations for multiple existing columns using auto-generated tags.
0983    /// \param[in] colNames set of names of the columns for which varied values are provided.
0984    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
0985    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
0986    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
0987    /// \param[in] inputColumns the names of the columns to be passed to the callable.
0988    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
0989    /// `"1"`, etc.
0990    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
0991    ///            colName is used if none is provided.
0992    ///
0993    /// This overload of Vary takes a list of column names as first argument.
0994    /// It takes an `nVariations` parameter instead of a list of tag names (`variationTags`). Tag names
0995    /// will be auto-generated as the sequence 0...``nVariations-1``.
0996    ///
0997    /// Example usage:
0998    /// ~~~{.cpp}
0999    /// auto nominal_hx =
1000    ///   df.Vary({"pt", "eta"}, // the columns that will vary simultaneously
1001    ///         [](double pt, double eta) { return RVec<RVecF>{{pt*0.9, pt*1.1}, {eta*0.9, eta*1.1}}; },
1002    ///         {"pt", "eta"},  // inputs to the Vary expression, independent of what columns are varied
1003    ///         2, // auto-generated variation tags
1004    ///         "ptAndEta")    // variation name
1005    ///     .Histo1D("pt", "eta");
1006    ///
1007    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
1008    /// hx["nominal"].Draw();
1009    /// hx["ptAndEta:0"].Draw("SAME");
1010    /// hx["ptAndEta:1"].Draw("SAME");
1011    /// ~~~
1012    ///
1013    /// \note See also This Vary() overload for more information.
1014    template <typename F>
1015    RInterface<Proxied> Vary(const std::vector<std::string> &colNames, F &&expression, const ColumnNames_t &inputColumns,
1016                             std::size_t nVariations, std::string_view variationName)
1017    {
1018       R__ASSERT(nVariations > 0 && "Must have at least one variation.");
1019 
1020       std::vector<std::string> variationTags;
1021       variationTags.reserve(nVariations);
1022       for (std::size_t i = 0u; i < nVariations; ++i)
1023          variationTags.emplace_back(std::to_string(i));
1024 
1025       return Vary(colNames, std::forward<F>(expression), inputColumns, std::move(variationTags), variationName);
1026    }
1027 
1028    /// \brief Register systematic variations for for multiple existing columns using custom variation tags.
1029    /// \param[in] colNames set of names of the columns for which varied values are provided.
1030    /// \param[in] expression a callable that evaluates the varied values for the specified columns. The callable can
1031    ///            take any column values as input, similarly to what happens during Filter and Define calls. It must
1032    ///            return an RVec of varied values, one for each variation tag, in the same order as the tags.
1033    /// \param[in] inputColumns the names of the columns to be passed to the callable.
1034    /// \param[in] inputColumns the names of the columns to be passed to the callable.
1035    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
1036    /// `"1"`, etc.
1037    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1038    ///            colName is used if none is provided.
1039    ///
1040    /// \note This overload ensures that the ambiguity between C++20 string, vector<string> construction from init list
1041    /// is avoided.
1042    ///
1043    /// \note See also This Vary() overload for more information.
1044    template <typename F>
1045    RInterface<Proxied> Vary(std::initializer_list<std::string> colNames, F &&expression,
1046                             const ColumnNames_t &inputColumns, std::size_t nVariations, std::string_view variationName)
1047    {
1048       return Vary(std::vector<std::string>(colNames), std::forward<F>(expression), inputColumns, nVariations, variationName);
1049    }
1050 
1051    /// \brief Register systematic variations for a single existing column using custom variation tags.
1052    /// \param[in] colName name of the column for which varied values are provided.
1053    /// \param[in] expression a string containing valid C++ code that evaluates to an RVec containing the varied
1054    ///            values for the specified column.
1055    /// \param[in] variationTags names for each of the varied values, e.g. `"up"` and `"down"`.
1056    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1057    ///            colName is used if none is provided.
1058    ///
1059    /// This overload adds the possibility for the expression used to evaluate the varied values to be just-in-time
1060    /// compiled. The example below shows how Vary() is used while dealing with a single column. The variation tags are
1061    /// defined as `{"down", "up"}`.
1062    /// ~~~{.cpp}
1063    /// auto nominal_hx =
1064    ///     df.Vary("pt", "ROOT::RVecD{pt*0.9, pt*1.1}", {"down", "up"})
1065    ///       .Filter("pt > k")
1066    ///       .Define("x", someFunc, {"pt"})
1067    ///       .Histo1D("x");
1068    ///
1069    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
1070    /// hx["nominal"].Draw();
1071    /// hx["pt:down"].Draw("SAME");
1072    /// hx["pt:up"].Draw("SAME");
1073    /// ~~~
1074    ///
1075    /// ## Short-hand expression syntax
1076    ///
1077    /// For convenience, when a C++ expression is passed to Vary, the return type can be omitted if the string begins
1078    /// with '{' and ends with '}' (whitespace, tab and newline characters are excluded from the search). This means that
1079    /// the following is equivalent to the example above:
1080    ///
1081    /// ~~~{.cpp}
1082    /// auto nominal_hx =
1083    ///     df.Vary("pt", "{pt*0.9, pt*1.1}", {"down", "up"})
1084    /// // Same as above
1085    /// ~~~
1086    ///
1087    /// \note See also This Vary() overload for more information.
1088    RInterface<Proxied> Vary(std::string_view colName, std::string_view expression,
1089                             const std::vector<std::string> &variationTags, std::string_view variationName = "")
1090    {
1091       std::vector<std::string> colNames{{std::string(colName)}};
1092       const std::string theVariationName{variationName.empty() ? colName : variationName};
1093 
1094       return JittedVaryImpl(colNames, expression, variationTags, theVariationName, /*isSingleColumn=*/true);
1095    }
1096 
1097    /// \brief Register systematic variations for a single existing column using auto-generated variation tags.
1098    /// \param[in] colName name of the column for which varied values are provided.
1099    /// \param[in] expression a string containing valid C++ code that evaluates to an RVec containing the varied
1100    ///            values for the specified column.
1101    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
1102    /// `"1"`, etc.
1103    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1104    ///            colName is used if none is provided.
1105    ///
1106    /// This overload adds the possibility for the expression used to evaluate the varied values to be a just-in-time
1107    /// compiled. The example below shows how Vary() is used while dealing with a single column. The variation tags are
1108    /// auto-generated.
1109    /// ~~~{.cpp}
1110    /// auto nominal_hx =
1111    ///     df.Vary("pt", "ROOT::RVecD{pt*0.9, pt*1.1}", 2)
1112    ///       .Histo1D("pt");
1113    ///
1114    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
1115    /// hx["nominal"].Draw();
1116    /// hx["pt:0"].Draw("SAME");
1117    /// hx["pt:1"].Draw("SAME");
1118    /// ~~~
1119    ///
1120    /// ## Short-hand expression syntax
1121    ///
1122    /// For convenience, when a C++ expression is passed to Vary, the return type can be omitted if the string begins
1123    /// with '{' and ends with '}' (whitespace, tab and newline characters are excluded from the search). This means that
1124    /// the following is equivalent to the example above:
1125    ///
1126    /// ~~~{.cpp}
1127    /// auto nominal_hx =
1128    ///     df.Vary("pt", "{pt*0.9, pt*1.1}", 2)
1129    /// // Same as above
1130    /// ~~~
1131    ///
1132    /// \note See also This Vary() overload for more information.
1133    RInterface<Proxied> Vary(std::string_view colName, std::string_view expression, std::size_t nVariations,
1134                             std::string_view variationName = "")
1135    {
1136       std::vector<std::string> variationTags;
1137       variationTags.reserve(nVariations);
1138       for (std::size_t i = 0u; i < nVariations; ++i)
1139          variationTags.emplace_back(std::to_string(i));
1140 
1141       return Vary(colName, expression, std::move(variationTags), variationName);
1142    }
1143 
1144    /// \brief Register systematic variations for multiple existing columns using auto-generated variation tags.
1145    /// \param[in] colNames set of names of the columns for which varied values are provided.
1146    /// \param[in] expression a string containing valid C++ code that evaluates to an RVec or RVecs containing the varied
1147    ///            values for the specified columns.
1148    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
1149    /// `"1"`, etc.
1150    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1151    ///
1152    /// This overload adds the possibility for the expression used to evaluate the varied values to be just-in-time
1153    /// compiled. It takes an nVariations parameter instead of a list of tag names.
1154    /// The varied results will be accessible via the keys of the dictionary with the form `variationName:N` where `N`
1155    /// is the corresponding sequential tag starting at 0 and going up to `nVariations - 1`.
1156    /// The example below shows how Vary() is used while dealing with multiple columns.
1157    ///
1158    /// ~~~{.cpp}
1159    /// auto nominal_hx =
1160    ///     df.Vary({"x", "y"}, "ROOT::RVec<ROOT::RVecD>{{x*0.9, x*1.1}, {y*0.9, y*1.1}}", 2, "xy")
1161    ///       .Histo1D("x", "y");
1162    ///
1163    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
1164    /// hx["nominal"].Draw();
1165    /// hx["xy:0"].Draw("SAME");
1166    /// hx["xy:1"].Draw("SAME");
1167    /// ~~~
1168    ///
1169    /// ## Short-hand expression syntax
1170    ///
1171    /// For convenience, when a C++ expression is passed to Vary, the return type can be omitted if the string begins
1172    /// with '{' and ends with '}' (whitespace, tab and newline characters are excluded from the search). This means that
1173    /// the following is equivalent to the example above:
1174    ///
1175    /// ~~~{.cpp}
1176    /// auto nominal_hx =
1177    ///     df.Vary("pt", "{{x*0.9, x*1.1}, {y*0.9, y*1.1}}", 2, "xy")
1178    /// // Same as above
1179    /// ~~~
1180    ///
1181    /// or also:
1182    ///
1183    /// ~~~{.cpp}
1184    /// auto nominal_hx =
1185    ///     df.Vary("pt", R"(
1186    ///    {
1187    ///     {x*0.9, x*1.1}, // x variations
1188    ///     {y*0.9, y*1.1}  // y variations
1189    ///    }
1190    ///      )", 2, "xy")
1191    /// // Same as above
1192    /// ~~~
1193    ///
1194    /// \note See also This Vary() overload for more information.
1195    RInterface<Proxied> Vary(const std::vector<std::string> &colNames, std::string_view expression,
1196                             std::size_t nVariations, std::string_view variationName)
1197    {
1198       std::vector<std::string> variationTags;
1199       variationTags.reserve(nVariations);
1200       for (std::size_t i = 0u; i < nVariations; ++i)
1201          variationTags.emplace_back(std::to_string(i));
1202 
1203       return Vary(colNames, expression, std::move(variationTags), variationName);
1204    }
1205 
1206    /// \brief Register systematic variations for multiple existing columns using auto-generated variation tags.
1207    /// \param[in] colNames set of names of the columns for which varied values are provided.
1208    /// \param[in] expression a string containing valid C++ code that evaluates to an RVec containing the varied
1209    ///            values for the specified column.
1210    /// \param[in] nVariations number of variations returned by the expression. The corresponding tags will be `"0"`,
1211    /// `"1"`, etc.
1212    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1213    ///            colName is used if none is provided.
1214    ///
1215    /// \note This overload ensures that the ambiguity between C++20 string, vector<string> construction from init list
1216    /// is avoided.
1217    ///
1218    /// \note See also This Vary() overload for more information.
1219    RInterface<Proxied> Vary(std::initializer_list<std::string> colNames, std::string_view expression,
1220                             std::size_t nVariations, std::string_view variationName)
1221    {
1222       return Vary(std::vector<std::string>(colNames), expression, nVariations, variationName);
1223    }
1224 
1225    /// \brief Register systematic variations for multiple existing columns using custom variation tags.
1226    /// \param[in] colNames set of names of the columns for which varied values are provided.
1227    /// \param[in] expression a string containing valid C++ code that evaluates to an RVec or RVecs containing the varied
1228    ///            values for the specified columns.
1229    /// \param[in] variationTags names for each of the varied values, e.g. `"up"` and `"down"`.
1230    /// \param[in] variationName a generic name for this set of varied values, e.g. `"ptvariation"`.
1231    ///
1232    /// This overload adds the possibility for the expression used to evaluate the varied values to be just-in-time
1233    /// compiled. The example below shows how Vary() is used while dealing with multiple columns. The tags are defined as
1234    /// `{"down", "up"}`.
1235    /// ~~~{.cpp}
1236    /// auto nominal_hx =
1237    ///     df.Vary({"x", "y"}, "ROOT::RVec<ROOT::RVecD>{{x*0.9, x*1.1}, {y*0.9, y*1.1}}", {"down", "up"}, "xy")
1238    ///       .Histo1D("x", "y");
1239    ///
1240    /// auto hx = ROOT::RDF::Experimental::VariationsFor(nominal_hx);
1241    /// hx["nominal"].Draw();
1242    /// hx["xy:down"].Draw("SAME");
1243    /// hx["xy:up"].Draw("SAME");
1244    /// ~~~
1245    ///
1246    /// ## Short-hand expression syntax
1247    ///
1248    /// For convenience, when a C++ expression is passed to Vary, the return type can be omitted if the string begins
1249    /// with '{' and ends with '}' (whitespace, tab and newline characters are excluded from the search). This means that
1250    /// the following is equivalent to the example above:
1251    ///
1252    /// ~~~{.cpp}
1253    /// auto nominal_hx =
1254    ///     df.Vary("pt", "{{x*0.9, x*1.1}, {y*0.9, y*1.1}}", {"down", "up"}, "xy")
1255    /// // Same as above
1256    /// ~~~
1257    ///
1258    /// or also:
1259    ///
1260    /// ~~~{.cpp}
1261    /// auto nominal_hx =
1262    ///     df.Vary("pt", R"(
1263    ///    {
1264    ///     {x*0.9, x*1.1}, // x variations
1265    ///     {y*0.9, y*1.1} // y variations
1266    ///    }
1267    ///      )", {"down", "up"}, "xy")
1268    /// // Same as above
1269    /// ~~~
1270    ///
1271    /// \note See also This Vary() overload for more information.
1272    RInterface<Proxied> Vary(const std::vector<std::string> &colNames, std::string_view expression,
1273                             const std::vector<std::string> &variationTags, std::string_view variationName)
1274    {
1275       return JittedVaryImpl(colNames, expression, variationTags, variationName, /*isSingleColumn=*/false);
1276    }
1277 
1278    ////////////////////////////////////////////////////////////////////////////
1279    /// \brief Allow to refer to a column with a different name.
1280    /// \param[in] alias name of the column alias
1281    /// \param[in] columnName of the column to be aliased
1282    /// \return the first node of the computation graph for which the alias is available.
1283    ///
1284    /// Aliasing an alias is supported.
1285    ///
1286    /// ### Example usage:
1287    /// ~~~{.cpp}
1288    /// auto df_with_alias = df.Alias("simple_name", "very_long&complex_name!!!");
1289    /// ~~~
1290    RInterface<Proxied> Alias(std::string_view alias, std::string_view columnName)
1291    {
1292       // The symmetry with Define is clear. We want to:
1293       // - Create globally the alias and return this very node, unchanged
1294       // - Make aliases accessible based on chains and not globally
1295 
1296       // Helper to find out if a name is a column
1297       auto &dsColumnNames = GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{};
1298 
1299       constexpr auto where = "Alias";
1300       RDFInternal::CheckValidCppVarName(alias, where);
1301       // If the alias name is a column name, there is a problem
1302       RDFInternal::CheckForRedefinition(where, alias, fColRegister, dsColumnNames);
1303 
1304       const auto validColumnName = GetValidatedColumnNames(1, {std::string(columnName)})[0];
1305 
1306       RDFInternal::RColumnRegister newCols(fColRegister);
1307       newCols.AddAlias(alias, validColumnName);
1308 
1309       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
1310 
1311       return newInterface;
1312    }
1313 
1314    template <typename... ColumnTypes>
1315    [[deprecated("Snapshot is not any more a template. You can safely remove the template parameters.")]]
1316    RResultPtr<RInterface<RLoopManager>>
1317    Snapshot(std::string_view treename, std::string_view filename, const ColumnNames_t &columnList,
1318             const RSnapshotOptions &options = RSnapshotOptions())
1319    {
1320       return Snapshot(treename, filename, columnList, options);
1321    }
1322 
1323    ////////////////////////////////////////////////////////////////////////////
1324    /// \brief Save selected columns to disk, in a new TTree or RNTuple `treename` in file `filename`.
1325    /// \param[in] treename The name of the output TTree or RNTuple.
1326    /// \param[in] filename The name of the output TFile.
1327    /// \param[in] columnList The list of names of the columns/branches/fields to be written.
1328    /// \param[in] options RSnapshotOptions struct with extra options to pass to TFile and TTree/RNTuple.
1329    /// \return a `RDataFrame` that wraps the snapshotted dataset.
1330    ///
1331    /// This function returns a `RDataFrame` built with the output TTree or RNTuple as a source.
1332    /// The types of the columns are automatically inferred and do not need to be specified.
1333    ///
1334    /// Support for writing of nested branches/fields is limited (although RDataFrame is able to read them) and dot ('.')
1335    /// characters in input column names will be replaced by underscores ('_') in the branches produced by Snapshot.
1336    /// When writing a variable size array through Snapshot, it is required that the column indicating its size is also
1337    /// written out and it appears before the array in the columnList.
1338    ///
1339    /// By default, in case of TTree, TChain or RNTuple inputs, Snapshot will try to write out all top-level branches.
1340    /// For other types of inputs, all columns returned by GetColumnNames() will be written out. Systematic variations of
1341    /// columns will be included if the corresponding flag is set in RSnapshotOptions. See \ref snapshot-with-variations
1342    /// "Snapshot with Variations" for more details. If friend trees or chains are present, by default all friend
1343    /// top-level branches that have names that do not collide with names of branches in the main TTree/TChain will be
1344    /// written out. Since v6.24, Snapshot will also write out friend branches with the same names of branches in the
1345    /// main TTree/TChain with names of the form
1346    /// `<friendname>_<branchname>` in order to differentiate them from the branches in the main tree/chain.
1347    ///
1348    /// ### Writing to a sub-directory
1349    ///
1350    /// Snapshot supports writing the TTree or RNTuple in a sub-directory inside the TFile. It is sufficient to specify
1351    /// the directory path as part of the TTree or RNTuple name, e.g. `df.Snapshot("subdir/t", "f.root")` writes TTree
1352    /// `t` in the sub-directory `subdir` of file `f.root` (creating file and sub-directory as needed).
1353    ///
1354    /// \attention In multi-thread runs (i.e. when EnableImplicitMT() has been called) threads will loop over clusters of
1355    /// entries in an undefined order, so Snapshot will produce outputs in which (clusters of) entries will be shuffled
1356    /// with respect to the input TTree. Using such "shuffled" TTrees as friends of the original trees would result in
1357    /// wrong associations between entries in the main TTree and entries in the "shuffled" friend. Since v6.22, ROOT will
1358    /// error out if such a "shuffled" TTree is used in a friendship.
1359    ///
1360    /// \note In case no events are written out (e.g. because no event passes all filters), Snapshot will still write the
1361    /// requested output TTree or RNTuple to the file, with all the branches requested to preserve the dataset schema.
1362    ///
1363    /// \note Snapshot will refuse to process columns with names of the form `#columnname`. These are special columns
1364    /// made available by some data sources (e.g. RNTupleDS) that represent the size of column `columnname`, and are
1365    /// not meant to be written out with that name (which is not a valid C++ variable name). Instead, go through an
1366    /// Alias(): `df.Alias("nbar", "#bar").Snapshot(..., {"nbar"})`.
1367    ///
1368    /// ### Example invocations:
1369    ///
1370    /// ~~~{.cpp}
1371    /// // No need to specify column types, they are automatically deduced thanks
1372    /// // to information coming from the data source
1373    /// df.Snapshot("outputTree", "outputFile.root", {"x", "y"});
1374    /// ~~~
1375    ///
1376    /// To book a Snapshot without triggering the event loop, one needs to set the appropriate flag in
1377    /// `RSnapshotOptions`:
1378    /// ~~~{.cpp}
1379    /// RSnapshotOptions opts;
1380    /// opts.fLazy = true;
1381    /// df.Snapshot("outputTree", "outputFile.root", {"x"}, opts);
1382    /// ~~~
1383    ///
1384    /// To snapshot to the RNTuple data format, the `fOutputFormat` option in `RSnapshotOptions` needs to be set
1385    /// accordingly:
1386    /// ~~~{.cpp}
1387    /// RSnapshotOptions opts;
1388    /// opts.fOutputFormat = ROOT::RDF::ESnapshotOutputFormat::kRNTuple;
1389    /// df.Snapshot("outputNTuple", "outputFile.root", {"x"}, opts);
1390    /// ~~~
1391    ///
1392    /// Snapshot systematic variations resulting from a Vary() call (see details \ref snapshot-with-variations "here"):
1393    /// ~~~{.cpp}
1394    /// RSnapshotOptions opts;
1395    /// opts.fIncludeVariations = true;
1396    /// df.Snapshot("outputTree", "outputFile.root", {"x"}, opts);
1397    /// ~~~
1398    RResultPtr<RInterface<RLoopManager>> Snapshot(std::string_view treename, std::string_view filename,
1399                                                  const ColumnNames_t &columnList,
1400                                                  const RSnapshotOptions &options = RSnapshotOptions())
1401    {
1402       // like columnList but with `#var` columns removed
1403       auto colListNoPoundSizes = RDFInternal::FilterArraySizeColNames(columnList, "Snapshot");
1404       // like columnListWithoutSizeColumns but with aliases resolved
1405       auto colListNoAliases = GetValidatedColumnNames(colListNoPoundSizes.size(), colListNoPoundSizes);
1406       RDFInternal::CheckForDuplicateSnapshotColumns(colListNoAliases);
1407       // like validCols but with missing size branches required by array branches added in the right positions
1408       const auto pairOfColumnLists =
1409          RDFInternal::AddSizeBranches(GetDataSource(), std::move(colListNoAliases), std::move(colListNoPoundSizes));
1410       const auto &colListNoAliasesWithSizeBranches = pairOfColumnLists.first;
1411       const auto &colListWithAliasesAndSizeBranches = pairOfColumnLists.second;
1412 
1413       const auto fullTreeName = treename;
1414       const auto parsedTreePath = RDFInternal::ParseTreePath(fullTreeName);
1415       treename = parsedTreePath.fTreeName;
1416       const auto &dirname = parsedTreePath.fDirName;
1417 
1418       ::TDirectory::TContext ctxt;
1419 
1420       RResultPtr<RInterface<RLoopManager>> resPtr;
1421 
1422       auto retrieveTypeID = [](const std::string &colName, const std::string &colTypeName,
1423                                bool isRNTuple = false) -> const std::type_info * {
1424          try {
1425             return &ROOT::Internal::RDF::TypeName2TypeID(colTypeName);
1426          } catch (const std::runtime_error &err) {
1427             if (isRNTuple)
1428                return &typeid(ROOT::Internal::RDF::UseNativeDataType);
1429 
1430             if (std::string(err.what()).find("Cannot extract type_info of type") != std::string::npos) {
1431                // We could not find RTTI for this column, thus we cannot write it out at the moment.
1432                std::string trueTypeName{colTypeName};
1433                if (colTypeName.rfind("CLING_UNKNOWN_TYPE", 0) == 0)
1434                   trueTypeName = colTypeName.substr(19);
1435                std::string msg{"No runtime type information is available for column \"" + colName +
1436                                "\" with type name \"" + trueTypeName +
1437                                "\". Thus, it cannot be written to disk with Snapshot. Make sure to generate and load "
1438                                "ROOT dictionaries for the type of this column."};
1439 
1440                throw std::runtime_error(msg);
1441             } else {
1442                throw;
1443             }
1444          }
1445       };
1446 
1447       RDFInternal::CheckSnapshotOptionsFormatCompatibility(options);
1448 
1449       if (options.fOutputFormat == ESnapshotOutputFormat::kRNTuple) {
1450          // The data source of the RNTuple resulting from the Snapshot action does not exist yet here, so we create one
1451          // without a data source for now, and set it once the actual data source can be created (i.e., after
1452          // writing the RNTuple).
1453          auto newRDF = std::make_shared<RInterface<RLoopManager>>(std::make_shared<RLoopManager>(colListNoPoundSizes));
1454 
1455          auto snapHelperArgs = std::make_shared<RDFInternal::SnapshotHelperArgs>(RDFInternal::SnapshotHelperArgs{
1456             std::string(filename), std::string(dirname), std::string(treename), colListWithAliasesAndSizeBranches,
1457             options, newRDF->GetLoopManager(), GetLoopManager(), true /* fToNTuple */, /*fIncludeVariations=*/false});
1458 
1459          auto &&nColumns = colListNoAliasesWithSizeBranches.size();
1460          const auto validColumnNames = GetValidatedColumnNames(nColumns, colListNoAliasesWithSizeBranches);
1461 
1462          const auto nSlots = fLoopManager->GetNSlots();
1463          std::vector<const std::type_info *> colTypeIDs;
1464          colTypeIDs.reserve(nColumns);
1465          for (decltype(nColumns) i{}; i < nColumns; i++) {
1466             const auto &colName = validColumnNames[i];
1467             const auto colTypeName = ROOT::Internal::RDF::ColumnName2ColumnTypeName(
1468                colName, /*tree*/ nullptr, GetDataSource(), fColRegister.GetDefine(colName), options.fVector2RVec);
1469             const std::type_info *colTypeID = retrieveTypeID(colName, colTypeName, /*isRNTuple*/ true);
1470             colTypeIDs.push_back(colTypeID);
1471          }
1472          // Crucial e.g. if the column names do not correspond to already-available column readers created by the data
1473          // source
1474          CheckAndFillDSColumns(validColumnNames, colTypeIDs);
1475 
1476          auto action =
1477             RDFInternal::BuildAction(validColumnNames, snapHelperArgs, nSlots, fProxiedPtr, fColRegister, colTypeIDs);
1478          resPtr = MakeResultPtr(newRDF, *GetLoopManager(), std::move(action));
1479       } else {
1480          if (RDFInternal::GetDataSourceLabel(*this) == "RNTupleDS" &&
1481              options.fOutputFormat == ESnapshotOutputFormat::kDefault) {
1482             Warning("Snapshot",
1483                     "The default Snapshot output data format is TTree, but the input data format is RNTuple. If you "
1484                     "want to Snapshot to RNTuple or suppress this warning, set the appropriate fOutputFormat option in "
1485                     "RSnapshotOptions. Note that this current default behaviour might change in the future.");
1486          }
1487 
1488          // We create an RLoopManager without a data source. This needs to be initialised when the output TTree dataset
1489          // has actually been created and written to TFile, i.e. at the end of the Snapshot execution.
1490          auto newRDF = std::make_shared<RInterface<RLoopManager>>(
1491             std::make_shared<RLoopManager>(colListNoAliasesWithSizeBranches));
1492 
1493          auto snapHelperArgs = std::make_shared<RDFInternal::SnapshotHelperArgs>(RDFInternal::SnapshotHelperArgs{
1494             std::string(filename), std::string(dirname), std::string(treename), colListWithAliasesAndSizeBranches,
1495             options, newRDF->GetLoopManager(), GetLoopManager(), false /* fToRNTuple */, options.fIncludeVariations});
1496 
1497          auto &&nColumns = colListNoAliasesWithSizeBranches.size();
1498          const auto validColumnNames = GetValidatedColumnNames(nColumns, colListNoAliasesWithSizeBranches);
1499 
1500          const auto nSlots = fLoopManager->GetNSlots();
1501          std::vector<const std::type_info *> colTypeIDs;
1502          colTypeIDs.reserve(nColumns);
1503          for (decltype(nColumns) i{}; i < nColumns; i++) {
1504             const auto &colName = validColumnNames[i];
1505             const auto colTypeName = ROOT::Internal::RDF::ColumnName2ColumnTypeName(
1506                colName, /*tree*/ nullptr, GetDataSource(), fColRegister.GetDefine(colName), options.fVector2RVec);
1507             const std::type_info *colTypeID = retrieveTypeID(colName, colTypeName);
1508             colTypeIDs.push_back(colTypeID);
1509          }
1510          // Crucial e.g. if the column names do not correspond to already-available column readers created by the data
1511          // source
1512          CheckAndFillDSColumns(validColumnNames, colTypeIDs);
1513 
1514          auto action =
1515             RDFInternal::BuildAction(validColumnNames, snapHelperArgs, nSlots, fProxiedPtr, fColRegister, colTypeIDs);
1516          resPtr = MakeResultPtr(newRDF, *GetLoopManager(), std::move(action));
1517       }
1518 
1519       if (!options.fLazy)
1520          *resPtr;
1521       return resPtr;
1522    }
1523 
1524    // clang-format off
1525    ////////////////////////////////////////////////////////////////////////////
1526    /// \brief Save selected columns to disk, in a new TTree or RNTuple `treename` in file `filename`.
1527    /// \param[in] treename The name of the output TTree or RNTuple.
1528    /// \param[in] filename The name of the output TFile.
1529    /// \param[in] columnNameRegexp The regular expression to match the column names to be selected. The presence of a '^' and a '$' at the end of the string is implicitly assumed if they are not specified. The dialect supported is PCRE via the TPRegexp class. An empty string signals the selection of all columns.
1530    /// \param[in] options RSnapshotOptions struct with extra options to pass to TFile and TTree/RNTuple
1531    /// \return a `RDataFrame` that wraps the snapshotted dataset.
1532    ///
1533    /// This function returns a `RDataFrame` built with the output TTree or RNTuple as a source.
1534    /// The types of the columns are automatically inferred and do not need to be specified.
1535    ///
1536    /// See Snapshot(std::string_view, std::string_view, const ColumnNames_t&, const RSnapshotOptions &) for a more complete description and example usages.
1537    RResultPtr<RInterface<RLoopManager>> Snapshot(std::string_view treename, std::string_view filename,
1538                                                  std::string_view columnNameRegexp = "",
1539                                                  const RSnapshotOptions &options = RSnapshotOptions())
1540    {
1541       const auto definedColumns = fColRegister.GenerateColumnNames();
1542 
1543       const auto dsColumns = GetDataSource() ? ROOT::Internal::RDF::GetTopLevelFieldNames(*GetDataSource()) : ColumnNames_t{};
1544       // Ignore R_rdf_sizeof_* columns coming from datasources: we don't want to Snapshot those
1545       ColumnNames_t dsColumnsWithoutSizeColumns;
1546       std::copy_if(dsColumns.begin(), dsColumns.end(), std::back_inserter(dsColumnsWithoutSizeColumns),
1547                    [](const std::string &name) { return name.size() < 13 || name.substr(0, 13) != "R_rdf_sizeof_"; });
1548       ColumnNames_t columnNames;
1549       columnNames.reserve(definedColumns.size() + dsColumnsWithoutSizeColumns.size());
1550       columnNames.insert(columnNames.end(), definedColumns.begin(), definedColumns.end());
1551       columnNames.insert(columnNames.end(), dsColumnsWithoutSizeColumns.begin(), dsColumnsWithoutSizeColumns.end());
1552 
1553       // The only way we can get duplicate entries is if a column coming from a tree or data-source is Redefine'd.
1554       // RemoveDuplicates should preserve ordering of the columns: it might be meaningful.
1555       RDFInternal::RemoveDuplicates(columnNames);
1556 
1557       std::vector<std::string> selectedColumns;
1558       try {
1559          selectedColumns = RDFInternal::ConvertRegexToColumns(columnNames, columnNameRegexp, "Snapshot");
1560       }
1561       catch (const std::runtime_error &e){
1562          // No columns were found, try again but consider all input data source columns
1563          if (auto ds = GetDataSource())
1564             selectedColumns = RDFInternal::ConvertRegexToColumns(ds->GetColumnNames(), columnNameRegexp, "Snapshot");
1565          else
1566             throw e;
1567       }
1568 
1569       if (RDFInternal::GetDataSourceLabel(*this) == "RNTupleDS") {
1570          RDFInternal::RemoveRNTupleSubfields(selectedColumns);
1571       }
1572 
1573       return Snapshot(treename, filename, selectedColumns, options);
1574    }
1575    // clang-format on
1576 
1577    // clang-format off
1578    ////////////////////////////////////////////////////////////////////////////
1579    /// \brief Save selected columns to disk, in a new TTree or RNTuple `treename` in file `filename`.
1580    /// \param[in] treename The name of the output TTree or RNTuple.
1581    /// \param[in] filename The name of the output TFile.
1582    /// \param[in] columnList The list of names of the columns/branches to be written.
1583    /// \param[in] options RSnapshotOptions struct with extra options to pass to TFile and TTree/RNTuple.
1584    /// \return a `RDataFrame` that wraps the snapshotted dataset.
1585    ///
1586    /// This function returns a `RDataFrame` built with the output TTree or RNTuple as a source.
1587    /// The types of the columns are automatically inferred and do not need to be specified.
1588    ///
1589    /// See Snapshot(std::string_view, std::string_view, const ColumnNames_t&, const RSnapshotOptions &) for a more complete description and example usages.
1590    RResultPtr<RInterface<RLoopManager>> Snapshot(std::string_view treename, std::string_view filename,
1591                                                  std::initializer_list<std::string> columnList,
1592                                                  const RSnapshotOptions &options = RSnapshotOptions())
1593    {
1594       ColumnNames_t selectedColumns(columnList);
1595       return Snapshot(treename, filename, selectedColumns, options);
1596    }
1597    // clang-format on
1598 
1599    ////////////////////////////////////////////////////////////////////////////
1600    /// \brief Save selected columns in memory.
1601    /// \tparam ColumnTypes variadic list of branch/column types.
1602    /// \param[in] columnList columns to be cached in memory.
1603    /// \return a `RDataFrame` that wraps the cached dataset.
1604    ///
1605    /// This action returns a new `RDataFrame` object, completely detached from
1606    /// the originating `RDataFrame`. The new dataframe only contains the cached
1607    /// columns and stores their content in memory for fast, zero-copy subsequent access.
1608    ///
1609    /// Use `Cache` if you know you will only need a subset of the (`Filter`ed) data that
1610    /// fits in memory and that will be accessed many times.
1611    ///
1612    /// \note Cache will refuse to process columns with names of the form `#columnname`. These are special columns
1613    /// made available by some data sources (e.g. RNTupleDS) that represent the size of column `columnname`, and are
1614    /// not meant to be written out with that name (which is not a valid C++ variable name). Instead, go through an
1615    /// Alias(): `df.Alias("nbar", "#bar").Cache<std::size_t>(..., {"nbar"})`.
1616    ///
1617    /// ### Example usage:
1618    ///
1619    /// **Types and columns specified:**
1620    /// ~~~{.cpp}
1621    /// auto cache_some_cols_df = df.Cache<double, MyClass, int>({"col0", "col1", "col2"});
1622    /// ~~~
1623    ///
1624    /// **Types inferred and columns specified (this invocation relies on jitting):**
1625    /// ~~~{.cpp}
1626    /// auto cache_some_cols_df = df.Cache({"col0", "col1", "col2"});
1627    /// ~~~
1628    ///
1629    /// **Types inferred and columns selected with a regexp (this invocation relies on jitting):**
1630    /// ~~~{.cpp}
1631    /// auto cache_all_cols_df = df.Cache(myRegexp);
1632    /// ~~~
1633    template <typename... ColumnTypes>
1634    RInterface<RLoopManager> Cache(const ColumnNames_t &columnList)
1635    {
1636       auto staticSeq = std::make_index_sequence<sizeof...(ColumnTypes)>();
1637       return CacheImpl<ColumnTypes...>(columnList, staticSeq);
1638    }
1639 
1640    ////////////////////////////////////////////////////////////////////////////
1641    /// \brief Save selected columns in memory.
1642    /// \param[in] columnList columns to be cached in memory
1643    /// \return a `RDataFrame` that wraps the cached dataset.
1644    ///
1645    /// See the previous overloads for more information.
1646    RInterface<RLoopManager> Cache(const ColumnNames_t &columnList)
1647    {
1648       // Early return: if the list of columns is empty, just return an empty RDF
1649       // If we proceed, the jitted call will not compile!
1650       if (columnList.empty()) {
1651          auto nEntries = *this->Count();
1652          RInterface<RLoopManager> emptyRDF(std::make_shared<RLoopManager>(nEntries));
1653          return emptyRDF;
1654       }
1655 
1656       std::stringstream cacheCall;
1657       auto upcastNode = RDFInternal::UpcastNode(fProxiedPtr);
1658       RInterface<TTraits::TakeFirstParameter_t<decltype(upcastNode)>> upcastInterface(fProxiedPtr, *fLoopManager,
1659                                                                                       fColRegister);
1660       // build a string equivalent to
1661       // "(RInterface<nodetype*>*)(this)->Cache<Ts...>(*(ColumnNames_t*)(&columnList))"
1662       RInterface<RLoopManager> resRDF(std::make_shared<ROOT::Detail::RDF::RLoopManager>(0));
1663       cacheCall << "*reinterpret_cast<ROOT::RDF::RInterface<ROOT::Detail::RDF::RLoopManager>*>("
1664                 << RDFInternal::PrettyPrintAddr(&resRDF)
1665                 << ") = reinterpret_cast<ROOT::RDF::RInterface<ROOT::Detail::RDF::RNodeBase>*>("
1666                 << RDFInternal::PrettyPrintAddr(&upcastInterface) << ")->Cache<";
1667 
1668       const auto columnListWithoutSizeColumns = RDFInternal::FilterArraySizeColNames(columnList, "Cache");
1669 
1670       const auto validColumnNames =
1671          GetValidatedColumnNames(columnListWithoutSizeColumns.size(), columnListWithoutSizeColumns);
1672       const auto colTypes =
1673          GetValidatedArgTypes(validColumnNames, fColRegister, nullptr, GetDataSource(), "Cache", /*vector2RVec=*/false);
1674       for (const auto &colType : colTypes)
1675          cacheCall << colType << ", ";
1676       if (!columnListWithoutSizeColumns.empty())
1677          cacheCall.seekp(-2, cacheCall.cur);                         // remove the last ",
1678       cacheCall << ">(*reinterpret_cast<std::vector<std::string>*>(" // vector<string> should be ColumnNames_t
1679                 << RDFInternal::PrettyPrintAddr(&columnListWithoutSizeColumns) << "));";
1680 
1681       // book the code to jit with the RLoopManager and trigger the event loop
1682       fLoopManager->ToJitExec(cacheCall.str());
1683       fLoopManager->Jit();
1684 
1685       return resRDF;
1686    }
1687 
1688    ////////////////////////////////////////////////////////////////////////////
1689    /// \brief Save selected columns in memory.
1690    /// \param[in] columnNameRegexp The regular expression to match the column names to be selected. The presence of a '^' and a '$' at the end of the string is implicitly assumed if they are not specified. The dialect supported is PCRE via the TPRegexp class. An empty string signals the selection of all columns.
1691    /// \return a `RDataFrame` that wraps the cached dataset.
1692    ///
1693    /// The existing columns are matched against the regular expression. If the string provided
1694    /// is empty, all columns are selected. See the previous overloads for more information.
1695    RInterface<RLoopManager> Cache(std::string_view columnNameRegexp = "")
1696    {
1697       const auto definedColumns = fColRegister.GenerateColumnNames();
1698       const auto dsColumns = GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{};
1699       // Ignore R_rdf_sizeof_* columns coming from datasources: we don't want to Snapshot those
1700       ColumnNames_t dsColumnsWithoutSizeColumns;
1701       std::copy_if(dsColumns.begin(), dsColumns.end(), std::back_inserter(dsColumnsWithoutSizeColumns),
1702                    [](const std::string &name) { return name.size() < 13 || name.substr(0, 13) != "R_rdf_sizeof_"; });
1703       ColumnNames_t columnNames;
1704       columnNames.reserve(definedColumns.size() + dsColumns.size());
1705       columnNames.insert(columnNames.end(), definedColumns.begin(), definedColumns.end());
1706       columnNames.insert(columnNames.end(), dsColumns.begin(), dsColumns.end());
1707       const auto selectedColumns = RDFInternal::ConvertRegexToColumns(columnNames, columnNameRegexp, "Cache");
1708       return Cache(selectedColumns);
1709    }
1710 
1711    ////////////////////////////////////////////////////////////////////////////
1712    /// \brief Save selected columns in memory.
1713    /// \param[in] columnList columns to be cached in memory.
1714    /// \return a `RDataFrame` that wraps the cached dataset.
1715    ///
1716    /// See the previous overloads for more information.
1717    RInterface<RLoopManager> Cache(std::initializer_list<std::string> columnList)
1718    {
1719       ColumnNames_t selectedColumns(columnList);
1720       return Cache(selectedColumns);
1721    }
1722 
1723    // clang-format off
1724    ////////////////////////////////////////////////////////////////////////////
1725    /// \brief Creates a node that filters entries based on range: [begin, end).
1726    /// \param[in] begin Initial entry number considered for this range.
1727    /// \param[in] end Final entry number (excluded) considered for this range. 0 means that the range goes until the end of the dataset.
1728    /// \param[in] stride Process one entry of the [begin, end) range every `stride` entries. Must be strictly greater than 0.
1729    /// \return the first node of the computation graph for which the event loop is limited to a certain range of entries.
1730    ///
1731    /// Note that in case of previous Ranges and Filters the selected range refers to the transformed dataset.
1732    /// Ranges are only available if EnableImplicitMT has _not_ been called. Multi-thread ranges are not supported.
1733    ///
1734    /// ### Example usage:
1735    /// ~~~{.cpp}
1736    /// auto d_0_30 = d.Range(0, 30); // Pick the first 30 entries
1737    /// auto d_15_end = d.Range(15, 0); // Pick all entries from 15 onwards
1738    /// auto d_15_end_3 = d.Range(15, 0, 3); // Stride: from event 15, pick an event every 3
1739    /// ~~~
1740    // clang-format on
1741    RInterface<RDFDetail::RRange<Proxied>> Range(unsigned int begin, unsigned int end, unsigned int stride = 1)
1742    {
1743       // check invariants
1744       if (stride == 0 || (end != 0 && end < begin))
1745          throw std::runtime_error("Range: stride must be strictly greater than 0 and end must be greater than begin.");
1746       CheckIMTDisabled("Range");
1747 
1748       using Range_t = RDFDetail::RRange<Proxied>;
1749       auto rangePtr = std::make_shared<Range_t>(begin, end, stride, fProxiedPtr);
1750       RInterface<RDFDetail::RRange<Proxied>> newInterface(std::move(rangePtr), *fLoopManager, fColRegister);
1751       return newInterface;
1752    }
1753 
1754    // clang-format off
1755    ////////////////////////////////////////////////////////////////////////////
1756    /// \brief Creates a node that filters entries based on range.
1757    /// \param[in] end Final entry number (excluded) considered for this range. 0 means that the range goes until the end of the dataset.
1758    /// \return a node of the computation graph for which the range is defined.
1759    ///
1760    /// See the other Range overload for a detailed description.
1761    // clang-format on
1762    RInterface<RDFDetail::RRange<Proxied>> Range(unsigned int end) { return Range(0, end, 1); }
1763 
1764    // clang-format off
1765    ////////////////////////////////////////////////////////////////////////////
1766    /// \brief Execute a user-defined function on each entry (*instant action*).
1767    /// \param[in] f Function, lambda expression, functor class or any other callable object performing user defined calculations.
1768    /// \param[in] columns Names of the columns/branches in input to the user function.
1769    ///
1770    /// The callable `f` is invoked once per entry. This is an *instant action*:
1771    /// upon invocation, an event loop as well as execution of all scheduled actions
1772    /// is triggered.
1773    /// Users are responsible for the thread-safety of this callable when executing
1774    /// with implicit multi-threading enabled (i.e. ROOT::EnableImplicitMT).
1775    ///
1776    /// ### Example usage:
1777    /// ~~~{.cpp}
1778    /// myDf.Foreach([](int i){ std::cout << i << std::endl;}, {"myIntColumn"});
1779    /// ~~~
1780    // clang-format on
1781    template <typename F>
1782    void Foreach(F f, const ColumnNames_t &columns = {})
1783    {
1784       using arg_types = typename TTraits::CallableTraits<decltype(f)>::arg_types_nodecay;
1785       using ret_type = typename TTraits::CallableTraits<decltype(f)>::ret_type;
1786       ForeachSlot(RDFInternal::AddSlotParameter<ret_type>(f, arg_types()), columns);
1787    }
1788 
1789    // clang-format off
1790    ////////////////////////////////////////////////////////////////////////////
1791    /// \brief Execute a user-defined function requiring a processing slot index on each entry (*instant action*).
1792    /// \param[in] f Function, lambda expression, functor class or any other callable object performing user defined calculations.
1793    /// \param[in] columns Names of the columns/branches in input to the user function.
1794    ///
1795    /// Same as `Foreach`, but the user-defined function takes an extra
1796    /// `unsigned int` as its first parameter, the *processing slot index*.
1797    /// This *slot index* will be assigned a different value, `0` to `poolSize - 1`,
1798    /// for each thread of execution.
1799    /// This is meant as a helper in writing thread-safe `Foreach`
1800    /// actions when using `RDataFrame` after `ROOT::EnableImplicitMT()`.
1801    /// The user-defined processing callable is able to follow different
1802    /// *streams of processing* indexed by the first parameter.
1803    /// `ForeachSlot` works just as well with single-thread execution: in that
1804    /// case `slot` will always be `0`.
1805    ///
1806    /// ### Example usage:
1807    /// ~~~{.cpp}
1808    /// myDf.ForeachSlot([](unsigned int s, int i){ std::cout << "Slot " << s << ": "<< i << std::endl;}, {"myIntColumn"});
1809    /// ~~~
1810    // clang-format on
1811    template <typename F>
1812    void ForeachSlot(F f, const ColumnNames_t &columns = {})
1813    {
1814       using ColTypes_t = TypeTraits::RemoveFirstParameter_t<typename TTraits::CallableTraits<F>::arg_types>;
1815       constexpr auto nColumns = ColTypes_t::list_size;
1816 
1817       const auto validColumnNames = GetValidatedColumnNames(nColumns, columns);
1818       CheckAndFillDSColumns(validColumnNames, ColTypes_t());
1819 
1820       using Helper_t = RDFInternal::ForeachSlotHelper<F>;
1821       using Action_t = RDFInternal::RAction<Helper_t, Proxied>;
1822 
1823       auto action = std::make_unique<Action_t>(Helper_t(std::move(f)), validColumnNames, fProxiedPtr, fColRegister);
1824 
1825       fLoopManager->Run();
1826    }
1827 
1828    // clang-format off
1829    ////////////////////////////////////////////////////////////////////////////
1830    /// \brief Execute a user-defined reduce operation on the values of a column.
1831    /// \tparam F The type of the reduce callable. Automatically deduced.
1832    /// \tparam T The type of the column to apply the reduction to. Automatically deduced.
1833    /// \param[in] f A callable with signature `T(T,T)`
1834    /// \param[in] columnName The column to be reduced. If omitted, the first default column is used instead.
1835    /// \return the reduced quantity wrapped in a ROOT::RDF:RResultPtr.
1836    ///
1837    /// A reduction takes two values of a column and merges them into one (e.g.
1838    /// by summing them, taking the maximum, etc). This action performs the
1839    /// specified reduction operation on all processed column values, returning
1840    /// a single value of the same type. The callable f must satisfy the general
1841    /// requirements of a *processing function* besides having signature `T(T,T)`
1842    /// where `T` is the type of column columnName.
1843    ///
1844    /// The returned reduced value of each thread (e.g. the initial value of a sum) is initialized to a
1845    /// default-constructed T object. This is commonly expected to be the neutral/identity element for the specific
1846    /// reduction operation `f` (e.g. 0 for a sum, 1 for a product). If a default-constructed T does not satisfy this
1847    /// requirement, users should explicitly specify an initialization value for T by calling the appropriate `Reduce`
1848    /// overload.
1849    ///
1850    /// ### Example usage:
1851    /// ~~~{.cpp}
1852    /// auto sumOfIntCol = d.Reduce([](int x, int y) { return x + y; }, "intCol");
1853    /// ~~~
1854    ///
1855    /// This action is *lazy*: upon invocation of this method the calculation is
1856    /// booked but not executed. Also see RResultPtr.
1857    // clang-format on
1858    template <typename F, typename T = typename TTraits::CallableTraits<F>::ret_type>
1859    RResultPtr<T> Reduce(F f, std::string_view columnName = "")
1860    {
1861       static_assert(
1862          std::is_default_constructible<T>::value,
1863          "reduce object cannot be default-constructed. Please provide an initialisation value (redIdentity)");
1864       return Reduce(std::move(f), columnName, T());
1865    }
1866 
1867    ////////////////////////////////////////////////////////////////////////////
1868    /// \brief Execute a user-defined reduce operation on the values of a column.
1869    /// \tparam F The type of the reduce callable. Automatically deduced.
1870    /// \tparam T The type of the column to apply the reduction to. Automatically deduced.
1871    /// \param[in] f A callable with signature `T(T,T)`
1872    /// \param[in] columnName The column to be reduced. If omitted, the first default column is used instead.
1873    /// \param[in] redIdentity The reduced object of each thread is initialized to this value.
1874    /// \return the reduced quantity wrapped in a RResultPtr.
1875    ///
1876    /// ### Example usage:
1877    /// ~~~{.cpp}
1878    /// auto sumOfIntColWithOffset = d.Reduce([](int x, int y) { return x + y; }, "intCol", 42);
1879    /// ~~~
1880    /// See the description of the first Reduce overload for more information.
1881    template <typename F, typename T = typename TTraits::CallableTraits<F>::ret_type>
1882    RResultPtr<T> Reduce(F f, std::string_view columnName, const T &redIdentity)
1883    {
1884       return Aggregate(f, f, columnName, redIdentity);
1885    }
1886 
1887    ////////////////////////////////////////////////////////////////////////////
1888    /// \brief Return the number of entries processed (*lazy action*).
1889    /// \return the number of entries wrapped in a RResultPtr.
1890    ///
1891    /// Useful e.g. for counting the number of entries passing a certain filter (see also `Report`).
1892    /// This action is *lazy*: upon invocation of this method the calculation is
1893    /// booked but not executed. Also see RResultPtr.
1894    ///
1895    /// ### Example usage:
1896    /// ~~~{.cpp}
1897    /// auto nEntriesAfterCuts = myFilteredDf.Count();
1898    /// ~~~
1899    ///
1900    RResultPtr<ULong64_t> Count()
1901    {
1902       const auto nSlots = fLoopManager->GetNSlots();
1903       auto cSPtr = std::make_shared<ULong64_t>(0);
1904       using Helper_t = RDFInternal::CountHelper;
1905       using Action_t = RDFInternal::RAction<Helper_t, Proxied>;
1906       auto action = std::make_unique<Action_t>(Helper_t(cSPtr, nSlots), ColumnNames_t({}), fProxiedPtr,
1907                                                RDFInternal::RColumnRegister(fColRegister));
1908       return MakeResultPtr(cSPtr, *fLoopManager, std::move(action));
1909    }
1910 
1911    ////////////////////////////////////////////////////////////////////////////
1912    /// \brief Return a collection of values of a column (*lazy action*, returns a std::vector by default).
1913    /// \tparam T The type of the column.
1914    /// \tparam COLL The type of collection used to store the values.
1915    /// \param[in] column The name of the column to collect the values of.
1916    /// \return the content of the selected column wrapped in a RResultPtr.
1917    ///
1918    /// The collection type to be specified for C-style array columns is `RVec<T>`:
1919    /// in this case the returned collection is a `std::vector<RVec<T>>`.
1920    /// ### Example usage:
1921    /// ~~~{.cpp}
1922    /// // In this case intCol is a std::vector<int>
1923    /// auto intCol = rdf.Take<int>("integerColumn");
1924    /// // Same content as above but in this case taken as a RVec<int>
1925    /// auto intColAsRVec = rdf.Take<int, RVec<int>>("integerColumn");
1926    /// // In this case intCol is a std::vector<RVec<int>>, a collection of collections
1927    /// auto cArrayIntCol = rdf.Take<RVec<int>>("cArrayInt");
1928    /// ~~~
1929    /// This action is *lazy*: upon invocation of this method the calculation is
1930    /// booked but not executed. Also see RResultPtr.
1931    template <typename T, typename COLL = std::vector<T>>
1932    RResultPtr<COLL> Take(std::string_view column = "")
1933    {
1934       const auto columns = column.empty() ? ColumnNames_t() : ColumnNames_t({std::string(column)});
1935 
1936       const auto validColumnNames = GetValidatedColumnNames(1, columns);
1937       CheckAndFillDSColumns(validColumnNames, TTraits::TypeList<T>());
1938 
1939       using Helper_t = RDFInternal::TakeHelper<T, T, COLL>;
1940       using Action_t = RDFInternal::RAction<Helper_t, Proxied>;
1941       auto valuesPtr = std::make_shared<COLL>();
1942       const auto nSlots = fLoopManager->GetNSlots();
1943 
1944       auto action =
1945          std::make_unique<Action_t>(Helper_t(valuesPtr, nSlots), validColumnNames, fProxiedPtr, fColRegister);
1946       return MakeResultPtr(valuesPtr, *fLoopManager, std::move(action));
1947    }
1948 
1949    ////////////////////////////////////////////////////////////////////////////
1950    /// \brief Fill and return a one-dimensional histogram with the values of a column (*lazy action*).
1951    /// \tparam V The type of the column used to fill the histogram.
1952    /// \param[in] model The returned histogram will be constructed using this as a model.
1953    /// \param[in] vName The name of the column that will fill the histogram.
1954    /// \return the monodimensional histogram wrapped in a RResultPtr.
1955    ///
1956    /// Columns can be of a container type (e.g. `std::vector<double>`), in which case the histogram
1957    /// is filled with each one of the elements of the container. In case multiple columns of container type
1958    /// are provided (e.g. values and weights) they must have the same length for each one of the events (but
1959    /// possibly different lengths between events).
1960    /// This action is *lazy*: upon invocation of this method the calculation is
1961    /// booked but not executed. Also see RResultPtr.
1962    ///
1963    /// ### Example usage:
1964    /// ~~~{.cpp}
1965    /// // Deduce column type (this invocation needs jitting internally)
1966    /// auto myHist1 = myDf.Histo1D({"histName", "histTitle", 64u, 0., 128.}, "myColumn");
1967    /// // Explicit column type
1968    /// auto myHist2 = myDf.Histo1D<float>({"histName", "histTitle", 64u, 0., 128.}, "myColumn");
1969    /// ~~~
1970    ///
1971    /// \note Differently from other ROOT interfaces, the returned histogram is not associated to gDirectory
1972    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
1973    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
1974    template <typename V = RDFDetail::RInferredType>
1975    RResultPtr<::TH1D> Histo1D(const TH1DModel &model = {"", "", 128u, 0., 0.}, std::string_view vName = "")
1976    {
1977       const auto userColumns = vName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(vName)});
1978 
1979       const auto validatedColumns = GetValidatedColumnNames(1, userColumns);
1980 
1981       std::shared_ptr<::TH1D> h(nullptr);
1982       {
1983          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
1984          h = model.GetHistogram();
1985       }
1986 
1987       if (h->GetXaxis()->GetXmax() == h->GetXaxis()->GetXmin())
1988          h->SetCanExtend(::TH1::kAllAxes);
1989       return CreateAction<RDFInternal::ActionTags::Histo1D, V>(validatedColumns, h, h, fProxiedPtr);
1990    }
1991 
1992    ////////////////////////////////////////////////////////////////////////////
1993    /// \brief Fill and return a one-dimensional histogram with the values of a column (*lazy action*).
1994    /// \tparam V The type of the column used to fill the histogram.
1995    /// \param[in] vName The name of the column that will fill the histogram.
1996    /// \return the monodimensional histogram wrapped in a RResultPtr.
1997    ///
1998    /// This overload uses a default model histogram TH1D(name, title, 128u, 0., 0.).
1999    /// The "name" and "title" strings are built starting from the input column name.
2000    /// See the description of the first Histo1D() overload for more details.
2001    ///
2002    /// ### Example usage:
2003    /// ~~~{.cpp}
2004    /// // Deduce column type (this invocation needs jitting internally)
2005    /// auto myHist1 = myDf.Histo1D("myColumn");
2006    /// // Explicit column type
2007    /// auto myHist2 = myDf.Histo1D<float>("myColumn");
2008    /// ~~~
2009    template <typename V = RDFDetail::RInferredType>
2010    RResultPtr<::TH1D> Histo1D(std::string_view vName)
2011    {
2012       const auto h_name = std::string(vName);
2013       const auto h_title = h_name + ";" + h_name + ";count";
2014       return Histo1D<V>({h_name.c_str(), h_title.c_str(), 128u, 0., 0.}, vName);
2015    }
2016 
2017    ////////////////////////////////////////////////////////////////////////////
2018    /// \brief Fill and return a one-dimensional histogram with the weighted values of a column (*lazy action*).
2019    /// \tparam V The type of the column used to fill the histogram.
2020    /// \tparam W The type of the column used as weights.
2021    /// \param[in] model The returned histogram will be constructed using this as a model.
2022    /// \param[in] vName The name of the column that will fill the histogram.
2023    /// \param[in] wName The name of the column that will provide the weights.
2024    /// \return the monodimensional histogram wrapped in a RResultPtr.
2025    ///
2026    /// See the description of the first Histo1D() overload for more details.
2027    ///
2028    /// ### Example usage:
2029    /// ~~~{.cpp}
2030    /// // Deduce column type (this invocation needs jitting internally)
2031    /// auto myHist1 = myDf.Histo1D({"histName", "histTitle", 64u, 0., 128.}, "myValue", "myweight");
2032    /// // Explicit column type
2033    /// auto myHist2 = myDf.Histo1D<float, int>({"histName", "histTitle", 64u, 0., 128.}, "myValue", "myweight");
2034    /// ~~~
2035    template <typename V = RDFDetail::RInferredType, typename W = RDFDetail::RInferredType>
2036    RResultPtr<::TH1D> Histo1D(const TH1DModel &model, std::string_view vName, std::string_view wName)
2037    {
2038       const std::vector<std::string_view> columnViews = {vName, wName};
2039       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2040                                   ? ColumnNames_t()
2041                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2042       std::shared_ptr<::TH1D> h(nullptr);
2043       {
2044          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2045          h = model.GetHistogram();
2046       }
2047 
2048       if (h->GetXaxis()->GetXmax() == h->GetXaxis()->GetXmin())
2049          h->SetCanExtend(::TH1::kAllAxes);
2050       return CreateAction<RDFInternal::ActionTags::Histo1D, V, W>(userColumns, h, h, fProxiedPtr);
2051    }
2052 
2053    ////////////////////////////////////////////////////////////////////////////
2054    /// \brief Fill and return a one-dimensional histogram with the weighted values of a column (*lazy action*).
2055    /// \tparam V The type of the column used to fill the histogram.
2056    /// \tparam W The type of the column used as weights.
2057    /// \param[in] vName The name of the column that will fill the histogram.
2058    /// \param[in] wName The name of the column that will provide the weights.
2059    /// \return the monodimensional histogram wrapped in a RResultPtr.
2060    ///
2061    /// This overload uses a default model histogram TH1D(name, title, 128u, 0., 0.).
2062    /// The "name" and "title" strings are built starting from the input column names.
2063    /// See the description of the first Histo1D() overload for more details.
2064    ///
2065    /// ### Example usage:
2066    /// ~~~{.cpp}
2067    /// // Deduce column types (this invocation needs jitting internally)
2068    /// auto myHist1 = myDf.Histo1D("myValue", "myweight");
2069    /// // Explicit column types
2070    /// auto myHist2 = myDf.Histo1D<float, int>("myValue", "myweight");
2071    /// ~~~
2072    template <typename V = RDFDetail::RInferredType, typename W = RDFDetail::RInferredType>
2073    RResultPtr<::TH1D> Histo1D(std::string_view vName, std::string_view wName)
2074    {
2075       // We build name and title based on the value and weight column names
2076       std::string str_vName{vName};
2077       std::string str_wName{wName};
2078       const auto h_name = str_vName + "_weighted_" + str_wName;
2079       const auto h_title = str_vName + ", weights: " + str_wName + ";" + str_vName + ";count * " + str_wName;
2080       return Histo1D<V, W>({h_name.c_str(), h_title.c_str(), 128u, 0., 0.}, vName, wName);
2081    }
2082 
2083    ////////////////////////////////////////////////////////////////////////////
2084    /// \brief Fill and return a one-dimensional histogram with the weighted values of a column (*lazy action*).
2085    /// \tparam V The type of the column used to fill the histogram.
2086    /// \tparam W The type of the column used as weights.
2087    /// \param[in] model The returned histogram will be constructed using this as a model.
2088    /// \return the monodimensional histogram wrapped in a RResultPtr.
2089    ///
2090    /// This overload will use the first two default columns as column names.
2091    /// See the description of the first Histo1D() overload for more details.
2092    template <typename V, typename W>
2093    RResultPtr<::TH1D> Histo1D(const TH1DModel &model = {"", "", 128u, 0., 0.})
2094    {
2095       return Histo1D<V, W>(model, "", "");
2096    }
2097 
2098    ////////////////////////////////////////////////////////////////////////////
2099    /// \brief Fill and return a two-dimensional histogram (*lazy action*).
2100    /// \tparam V1 The type of the column used to fill the x axis of the histogram.
2101    /// \tparam V2 The type of the column used to fill the y axis of the histogram.
2102    /// \param[in] model The returned histogram will be constructed using this as a model.
2103    /// \param[in] v1Name The name of the column that will fill the x axis.
2104    /// \param[in] v2Name The name of the column that will fill the y axis.
2105    /// \return the bidimensional histogram wrapped in a RResultPtr.
2106    ///
2107    /// Columns can be of a container type (e.g. std::vector<double>), in which case the histogram
2108    /// is filled with each one of the elements of the container. In case multiple columns of container type
2109    /// are provided (e.g. values and weights) they must have the same length for each one of the events (but
2110    /// possibly different lengths between events).
2111    /// This action is *lazy*: upon invocation of this method the calculation is
2112    /// booked but not executed. Also see RResultPtr.
2113    ///
2114    /// ### Example usage:
2115    /// ~~~{.cpp}
2116    /// // Deduce column types (this invocation needs jitting internally)
2117    /// auto myHist1 = myDf.Histo2D({"histName", "histTitle", 64u, 0., 128., 32u, -4., 4.}, "myValueX", "myValueY");
2118    /// // Explicit column types
2119    /// auto myHist2 = myDf.Histo2D<float, float>({"histName", "histTitle", 64u, 0., 128., 32u, -4., 4.}, "myValueX", "myValueY");
2120    /// ~~~
2121    ///
2122    ///
2123    /// \note Differently from other ROOT interfaces, the returned histogram is not associated to gDirectory
2124    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
2125    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
2126    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType>
2127    RResultPtr<::TH2D> Histo2D(const TH2DModel &model, std::string_view v1Name = "", std::string_view v2Name = "")
2128    {
2129       std::shared_ptr<::TH2D> h(nullptr);
2130       {
2131          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2132          h = model.GetHistogram();
2133       }
2134       if (!RDFInternal::HistoUtils<::TH2D>::HasAxisLimits(*h)) {
2135          throw std::runtime_error("2D histograms with no axes limits are not supported yet.");
2136       }
2137       const std::vector<std::string_view> columnViews = {v1Name, v2Name};
2138       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2139                                   ? ColumnNames_t()
2140                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2141       return CreateAction<RDFInternal::ActionTags::Histo2D, V1, V2>(userColumns, h, h, fProxiedPtr);
2142    }
2143 
2144    ////////////////////////////////////////////////////////////////////////////
2145    /// \brief Fill and return a weighted two-dimensional histogram (*lazy action*).
2146    /// \tparam V1 The type of the column used to fill the x axis of the histogram.
2147    /// \tparam V2 The type of the column used to fill the y axis of the histogram.
2148    /// \tparam W The type of the column used for the weights of the histogram.
2149    /// \param[in] model The returned histogram will be constructed using this as a model.
2150    /// \param[in] v1Name The name of the column that will fill the x axis.
2151    /// \param[in] v2Name The name of the column that will fill the y axis.
2152    /// \param[in] wName The name of the column that will provide the weights.
2153    /// \return the bidimensional histogram wrapped in a RResultPtr.
2154    ///
2155    /// This action is *lazy*: upon invocation of this method the calculation is
2156    /// booked but not executed. Also see RResultPtr.
2157    ///
2158    /// ### Example usage:
2159    /// ~~~{.cpp}
2160    /// // Deduce column types (this invocation needs jitting internally)
2161    /// auto myHist1 = myDf.Histo2D({"histName", "histTitle", 64u, 0., 128., 32u, -4., 4.}, "myValueX", "myValueY", "myWeight");
2162    /// // Explicit column types
2163    /// auto myHist2 = myDf.Histo2D<float, float, double>({"histName", "histTitle", 64u, 0., 128., 32u, -4., 4.}, "myValueX", "myValueY", "myWeight");
2164    /// ~~~
2165    ///
2166    /// See the documentation of the first Histo2D() overload for more details.
2167    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
2168              typename W = RDFDetail::RInferredType>
2169    RResultPtr<::TH2D>
2170    Histo2D(const TH2DModel &model, std::string_view v1Name, std::string_view v2Name, std::string_view wName)
2171    {
2172       std::shared_ptr<::TH2D> h(nullptr);
2173       {
2174          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2175          h = model.GetHistogram();
2176       }
2177       if (!RDFInternal::HistoUtils<::TH2D>::HasAxisLimits(*h)) {
2178          throw std::runtime_error("2D histograms with no axes limits are not supported yet.");
2179       }
2180       const std::vector<std::string_view> columnViews = {v1Name, v2Name, wName};
2181       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2182                                   ? ColumnNames_t()
2183                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2184       return CreateAction<RDFInternal::ActionTags::Histo2D, V1, V2, W>(userColumns, h, h, fProxiedPtr);
2185    }
2186 
2187    template <typename V1, typename V2, typename W>
2188    RResultPtr<::TH2D> Histo2D(const TH2DModel &model)
2189    {
2190       return Histo2D<V1, V2, W>(model, "", "", "");
2191    }
2192 
2193    ////////////////////////////////////////////////////////////////////////////
2194    /// \brief Fill and return a three-dimensional histogram (*lazy action*).
2195    /// \tparam V1 The type of the column used to fill the x axis of the histogram. Inferred if not present.
2196    /// \tparam V2 The type of the column used to fill the y axis of the histogram. Inferred if not present.
2197    /// \tparam V3 The type of the column used to fill the z axis of the histogram. Inferred if not present.
2198    /// \param[in] model The returned histogram will be constructed using this as a model.
2199    /// \param[in] v1Name The name of the column that will fill the x axis.
2200    /// \param[in] v2Name The name of the column that will fill the y axis.
2201    /// \param[in] v3Name The name of the column that will fill the z axis.
2202    /// \return the tridimensional histogram wrapped in a RResultPtr.
2203    ///
2204    /// This action is *lazy*: upon invocation of this method the calculation is
2205    /// booked but not executed. Also see RResultPtr.
2206    ///
2207    /// ### Example usage:
2208    /// ~~~{.cpp}
2209    /// // Deduce column types (this invocation needs jitting internally)
2210    /// auto myHist1 = myDf.Histo3D({"name", "title", 64u, 0., 128., 32u, -4., 4., 8u, -2., 2.},
2211    ///                             "myValueX", "myValueY", "myValueZ");
2212    /// // Explicit column types
2213    /// auto myHist2 = myDf.Histo3D<double, double, float>({"name", "title", 64u, 0., 128., 32u, -4., 4., 8u, -2., 2.},
2214    ///                                                    "myValueX", "myValueY", "myValueZ");
2215    /// ~~~
2216    /// \note If three-dimensional histograms consume too much memory in multithreaded runs, the cloning of TH3D
2217    /// per thread can be reduced using ROOT::RDF::Experimental::ThreadsPerTH3(). See the section "Memory Usage" in
2218    /// the RDataFrame description.
2219    /// \note Differently from other ROOT interfaces, the returned histogram is not associated to gDirectory
2220    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
2221    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
2222    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
2223              typename V3 = RDFDetail::RInferredType>
2224    RResultPtr<::TH3D> Histo3D(const TH3DModel &model, std::string_view v1Name = "", std::string_view v2Name = "",
2225                               std::string_view v3Name = "")
2226    {
2227       std::shared_ptr<::TH3D> h(nullptr);
2228       {
2229          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2230          h = model.GetHistogram();
2231       }
2232       if (!RDFInternal::HistoUtils<::TH3D>::HasAxisLimits(*h)) {
2233          throw std::runtime_error("3D histograms with no axes limits are not supported yet.");
2234       }
2235       const std::vector<std::string_view> columnViews = {v1Name, v2Name, v3Name};
2236       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2237                                   ? ColumnNames_t()
2238                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2239       return CreateAction<RDFInternal::ActionTags::Histo3D, V1, V2, V3>(userColumns, h, h, fProxiedPtr);
2240    }
2241 
2242    ////////////////////////////////////////////////////////////////////////////
2243    /// \brief Fill and return a three-dimensional histogram (*lazy action*).
2244    /// \tparam V1 The type of the column used to fill the x axis of the histogram. Inferred if not present.
2245    /// \tparam V2 The type of the column used to fill the y axis of the histogram. Inferred if not present.
2246    /// \tparam V3 The type of the column used to fill the z axis of the histogram. Inferred if not present.
2247    /// \tparam W The type of the column used for the weights of the histogram. Inferred if not present.
2248    /// \param[in] model The returned histogram will be constructed using this as a model.
2249    /// \param[in] v1Name The name of the column that will fill the x axis.
2250    /// \param[in] v2Name The name of the column that will fill the y axis.
2251    /// \param[in] v3Name The name of the column that will fill the z axis.
2252    /// \param[in] wName The name of the column that will provide the weights.
2253    /// \return the tridimensional histogram wrapped in a RResultPtr.
2254    ///
2255    /// This action is *lazy*: upon invocation of this method the calculation is
2256    /// booked but not executed. Also see RResultPtr.
2257    ///
2258    /// ### Example usage:
2259    /// ~~~{.cpp}
2260    /// // Deduce column types (this invocation needs jitting internally)
2261    /// auto myHist1 = myDf.Histo3D({"name", "title", 64u, 0., 128., 32u, -4., 4., 8u, -2., 2.},
2262    ///                             "myValueX", "myValueY", "myValueZ", "myWeight");
2263    /// // Explicit column types
2264    /// using d_t = double;
2265    /// auto myHist2 = myDf.Histo3D<d_t, d_t, float, d_t>({"name", "title", 64u, 0., 128., 32u, -4., 4., 8u, -2., 2.},
2266    ///                                                    "myValueX", "myValueY", "myValueZ", "myWeight");
2267    /// ~~~
2268    ///
2269    ///
2270    /// See the documentation of the first Histo2D() overload for more details.
2271    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
2272              typename V3 = RDFDetail::RInferredType, typename W = RDFDetail::RInferredType>
2273    RResultPtr<::TH3D> Histo3D(const TH3DModel &model, std::string_view v1Name, std::string_view v2Name,
2274                               std::string_view v3Name, std::string_view wName)
2275    {
2276       std::shared_ptr<::TH3D> h(nullptr);
2277       {
2278          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2279          h = model.GetHistogram();
2280       }
2281       if (!RDFInternal::HistoUtils<::TH3D>::HasAxisLimits(*h)) {
2282          throw std::runtime_error("3D histograms with no axes limits are not supported yet.");
2283       }
2284       const std::vector<std::string_view> columnViews = {v1Name, v2Name, v3Name, wName};
2285       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2286                                   ? ColumnNames_t()
2287                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2288       return CreateAction<RDFInternal::ActionTags::Histo3D, V1, V2, V3, W>(userColumns, h, h, fProxiedPtr);
2289    }
2290 
2291    template <typename V1, typename V2, typename V3, typename W>
2292    RResultPtr<::TH3D> Histo3D(const TH3DModel &model)
2293    {
2294       return Histo3D<V1, V2, V3, W>(model, "", "", "", "");
2295    }
2296 
2297    ////////////////////////////////////////////////////////////////////////////
2298    /// \brief Fill and return an N-dimensional histogram (*lazy action*).
2299    /// \tparam FirstColumn The first type of the column the values of which are used to fill the object. Inferred if not
2300    /// present.
2301    /// \tparam OtherColumns A list of the other types of the columns the values of which are used to fill the
2302    /// object.
2303    /// \param[in] model The returned histogram will be constructed using this as a model.
2304    /// \param[in] columnList
2305    /// A list containing the names of the columns that will be passed when calling `Fill`.
2306    /// \param[in] wName The name of the column that will provide the weights.
2307    /// \return the N-dimensional histogram wrapped in a RResultPtr.
2308    ///
2309    /// This action is *lazy*: upon invocation of this method the calculation is
2310    /// booked but not executed. See RResultPtr documentation.
2311    ///
2312    /// ### Example usage:
2313    /// ~~~{.cpp}
2314    /// auto myFilledObj = myDf.HistoND<float, float, float, float>({"name","title", 4,
2315    ///                                                {40,40,40,40}, {20.,20.,20.,20.}, {60.,60.,60.,60.}},
2316    ///                                               {"col0", "col1", "col2", "col3"});
2317    /// ~~~
2318    ///
2319    /// \note A column with event weights should not be passed as part of `columnList`, but instead be passed in the new
2320    /// argument `wName`: `HistoND(model, cols, weightCol)`.
2321    ///
2322    template <typename FirstColumn, typename... OtherColumns> // need FirstColumn to disambiguate overloads
2323    RResultPtr<::THnD> HistoND(const THnDModel &model, const ColumnNames_t &columnList, std::string_view wName = "")
2324    {
2325       std::shared_ptr<::THnD> h(nullptr);
2326       {
2327          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2328          h = model.GetHistogram();
2329          const auto hDims = h->GetNdimensions();
2330          decltype(hDims) nCols = columnList.size();
2331 
2332          if (!wName.empty() && nCols == hDims + 1)
2333             throw std::invalid_argument("The weight column was passed as an argument and at the same time the list of "
2334                                         "input columns contains one column more than the number of dimensions of the "
2335                                         "histogram. Call as 'HistoND(model, cols, weightCol)'.");
2336 
2337          if (nCols == hDims + 1)
2338             Warning("HistoND", "Passing the column with the weights as the last column in the list is deprecated. "
2339                                "Instead, pass it as a separate argument, e.g. 'HistoND(model, cols, weightCol)'.");
2340 
2341          if (!wName.empty() || nCols == hDims + 1)
2342             h->Sumw2();
2343 
2344          if (nCols != hDims + 1 && nCols != hDims)
2345             throw std::invalid_argument("Wrong number of columns for the specified number of histogram axes.");
2346       }
2347 
2348       if (!wName.empty()) {
2349          // The action helper will invoke THnBase::Fill overload that performs weighted filling in case the number of
2350          // passed arguments is one more the number of dimensions of the histogram.
2351          ColumnNames_t userColumns = columnList;
2352          userColumns.push_back(std::string{wName});
2353          return CreateAction<RDFInternal::ActionTags::HistoND, FirstColumn, OtherColumns...>(userColumns, h, h,
2354                                                                                              fProxiedPtr);
2355       }
2356       return CreateAction<RDFInternal::ActionTags::HistoND, FirstColumn, OtherColumns...>(columnList, h, h,
2357                                                                                           fProxiedPtr);
2358    }
2359 
2360    ////////////////////////////////////////////////////////////////////////////
2361    /// \brief Fill and return an N-dimensional histogram (*lazy action*).
2362    /// \param[in] model The returned histogram will be constructed using this as a model.
2363    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2364    /// \param[in] wName The name of the column that will provide the weights.
2365    /// \return the N-dimensional histogram wrapped in a RResultPtr.
2366    ///
2367    /// This action is *lazy*: upon invocation of this method the calculation is
2368    /// booked but not executed. Also see RResultPtr.
2369    ///
2370    /// ### Example usage:
2371    /// ~~~{.cpp}
2372    /// auto myFilledObj = myDf.HistoND({"name","title", 4,
2373    ///                                                {40,40,40,40}, {20.,20.,20.,20.}, {60.,60.,60.,60.}},
2374    ///                                               {"col0", "col1", "col2", "col3"});
2375    /// ~~~
2376    ///
2377    /// \note A column with event weights should not be passed as part of `columnList`, but instead be passed in the new
2378    /// argument `wName`: `HistoND(model, cols, weightCol)`.
2379    ///
2380    RResultPtr<::THnD> HistoND(const THnDModel &model, const ColumnNames_t &columnList, std::string_view wName = "")
2381    {
2382       std::shared_ptr<::THnD> h(nullptr);
2383       {
2384          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2385          h = model.GetHistogram();
2386          const auto hDims = h->GetNdimensions();
2387          decltype(hDims) nCols = columnList.size();
2388 
2389          if (!wName.empty() && nCols == hDims + 1)
2390             throw std::invalid_argument("The weight column was passed as an argument and at the same time the list of "
2391                                         "input columns contains one column more than the number of dimensions of the "
2392                                         "histogram. Call as 'HistoND(model, cols, weightCol)'.");
2393 
2394          if (nCols == hDims + 1)
2395             Warning("HistoND", "Passing the column with the weights as the last column in the list is deprecated. "
2396                                "Instead, pass it as a separate argument, e.g. 'HistoND(model, cols, weightCol)'.");
2397 
2398          if (!wName.empty() || nCols == hDims + 1)
2399             h->Sumw2();
2400 
2401          if (nCols != hDims + 1 && nCols != hDims)
2402             throw std::invalid_argument("Wrong number of columns for the specified number of histogram axes.");
2403       }
2404 
2405       if (!wName.empty()) {
2406          // The action helper will invoke THnBase::Fill overload that performs weighted filling in case the number of
2407          // passed arguments is one more the number of dimensions of the histogram.
2408          ColumnNames_t userColumns = columnList;
2409          userColumns.push_back(std::string{wName});
2410          return CreateAction<RDFInternal::ActionTags::HistoND, RDFDetail::RInferredType>(userColumns, h, h, fProxiedPtr,
2411                                                                                          userColumns.size());
2412       }
2413       return CreateAction<RDFInternal::ActionTags::HistoND, RDFDetail::RInferredType>(columnList, h, h, fProxiedPtr,
2414                                                                                       columnList.size());
2415    }
2416 
2417    ////////////////////////////////////////////////////////////////////////////
2418    /// \brief Fill and return a sparse N-dimensional histogram (*lazy action*).
2419    /// \tparam FirstColumn The first type of the column the values of which are used to fill the object. Inferred if not
2420    /// present.
2421    /// \tparam OtherColumns A list of the other types of the columns the values of which are used to fill the
2422    /// object.
2423    /// \param[in] model The returned histogram will be constructed using this as a model.
2424    /// \param[in] columnList
2425    /// A list containing the names of the columns that will be passed when calling `Fill`.
2426    /// \param[in] wName The name of the column that will provide the weights.
2427    /// \return the N-dimensional histogram wrapped in a RResultPtr.
2428    ///
2429    /// This action is *lazy*: upon invocation of this method the calculation is
2430    /// booked but not executed. See RResultPtr documentation.
2431    ///
2432    /// ### Example usage:
2433    /// ~~~{.cpp}
2434    /// auto myFilledObj = myDf.HistoNSparseD<float, float, float, float>({"name","title", 4,
2435    ///                                                {40,40,40,40}, {20.,20.,20.,20.}, {60.,60.,60.,60.}},
2436    ///                                               {"col0", "col1", "col2", "col3"});
2437    /// ~~~
2438    ///
2439    /// \note A column with event weights should not be passed as part of `columnList`, but instead be passed in the new
2440    /// argument `wName`: `HistoND(model, cols, weightCol)`.
2441    ///
2442    template <typename FirstColumn, typename... OtherColumns> // need FirstColumn to disambiguate overloads
2443    RResultPtr<::THnSparseD>
2444    HistoNSparseD(const THnSparseDModel &model, const ColumnNames_t &columnList, std::string_view wName = "")
2445    {
2446       std::shared_ptr<::THnSparseD> h(nullptr);
2447       {
2448          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2449          h = model.GetHistogram();
2450          const auto hDims = h->GetNdimensions();
2451          decltype(hDims) nCols = columnList.size();
2452 
2453          if (!wName.empty() && nCols == hDims + 1)
2454             throw std::invalid_argument("The weight column was passed as an argument and at the same time the list of "
2455                                         "input columns contains one column more than the number of dimensions of the "
2456                                         "histogram. Call as 'HistoNSparseD(model, cols, weightCol)'.");
2457 
2458          if (nCols == hDims + 1)
2459             Warning("HistoNSparseD",
2460                     "Passing the column with the weights as the last column in the list is deprecated. "
2461                     "Instead, pass it as a separate argument, e.g. 'HistoNSparseD(model, cols, weightCol)'.");
2462 
2463          if (!wName.empty() || nCols == hDims + 1)
2464             h->Sumw2();
2465 
2466          if (nCols != hDims + 1 && nCols != hDims)
2467             throw std::invalid_argument("Wrong number of columns for the specified number of histogram axes.");
2468       }
2469 
2470       if (!wName.empty()) {
2471          // The action helper will invoke THnBase::Fill overload that performs weighted filling in case the number of
2472          // passed arguments is one more the number of dimensions of the histogram.
2473          ColumnNames_t userColumns = columnList;
2474          userColumns.push_back(std::string{wName});
2475          return CreateAction<RDFInternal::ActionTags::HistoNSparseD, FirstColumn, OtherColumns...>(userColumns, h, h,
2476                                                                                                    fProxiedPtr);
2477       }
2478       return CreateAction<RDFInternal::ActionTags::HistoNSparseD, FirstColumn, OtherColumns...>(columnList, h, h,
2479                                                                                                 fProxiedPtr);
2480    }
2481 
2482    ////////////////////////////////////////////////////////////////////////////
2483    /// \brief Fill and return a sparse N-dimensional histogram (*lazy action*).
2484    /// \param[in] model The returned histogram will be constructed using this as a model.
2485    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2486    /// \param[in] wName The name of the column that will provide the weights.
2487    /// \return the N-dimensional histogram wrapped in a RResultPtr.
2488    ///
2489    /// This action is *lazy*: upon invocation of this method the calculation is
2490    /// booked but not executed. Also see RResultPtr.
2491    ///
2492    /// ### Example usage:
2493    /// ~~~{.cpp}
2494    /// auto myFilledObj = myDf.HistoNSparseD({"name","title", 4,
2495    ///                                                {40,40,40,40}, {20.,20.,20.,20.}, {60.,60.,60.,60.}},
2496    ///                                               {"col0", "col1", "col2", "col3"});
2497    /// ~~~
2498    ///
2499    /// \note A column with event weights should not be passed as part of `columnList`, but instead be passed in the new
2500    /// argument `wName`: `HistoND(model, cols, weightCol)`.
2501    ///
2502    RResultPtr<::THnSparseD>
2503    HistoNSparseD(const THnSparseDModel &model, const ColumnNames_t &columnList, std::string_view wName = "")
2504    {
2505       std::shared_ptr<::THnSparseD> h(nullptr);
2506       {
2507          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2508          h = model.GetHistogram();
2509          const auto hDims = h->GetNdimensions();
2510          decltype(hDims) nCols = columnList.size();
2511 
2512          if (!wName.empty() && nCols == hDims + 1)
2513             throw std::invalid_argument("The weight column was passed as an argument and at the same time the list of "
2514                                         "input columns contains one column more than the number of dimensions of the "
2515                                         "histogram. Call as 'HistoNSparseD(model, cols, weightCol)'.");
2516 
2517          if (nCols == hDims + 1)
2518             Warning("HistoNSparseD",
2519                     "Passing the column with the weights as the last column in the list is deprecated. "
2520                     "Instead, pass it as a separate argument, e.g. 'HistoNSparseD(model, cols, weightCol)'.");
2521 
2522          if (!wName.empty() || nCols == hDims + 1)
2523             h->Sumw2();
2524 
2525          if (nCols != hDims + 1 && nCols != hDims)
2526             throw std::invalid_argument("Wrong number of columns for the specified number of histogram axes.");
2527       }
2528 
2529       if (!wName.empty()) {
2530          // The action helper will invoke THnBase::Fill overload that performs weighted filling in case the number of
2531          // passed arguments is one more the number of dimensions of the histogram.
2532          ColumnNames_t userColumns = columnList;
2533          userColumns.push_back(std::string{wName});
2534          return CreateAction<RDFInternal::ActionTags::HistoNSparseD, RDFDetail::RInferredType>(
2535             userColumns, h, h, fProxiedPtr, userColumns.size());
2536       }
2537       return CreateAction<RDFInternal::ActionTags::HistoNSparseD, RDFDetail::RInferredType>(
2538          columnList, h, h, fProxiedPtr, columnList.size());
2539    }
2540 
2541 #ifdef R__HAS_ROOT7
2542    ////////////////////////////////////////////////////////////////////////////
2543    /// \brief Fill and return a one-dimensional RHist (*lazy action*).
2544    /// \tparam BinContentType The bin content type of the returned RHist.
2545    /// \param[in] nNormalBins The returned histogram will be constructed using this number of normal bins.
2546    /// \param[in] interval The axis interval of the constructed histogram (lower end inclusive, upper end exclusive).
2547    /// \param[in] vName The name of the column that will fill the histogram.
2548    /// \return the histogram wrapped in a RResultPtr.
2549    ///
2550    /// This action is *lazy*: upon invocation of this method the calculation is
2551    /// booked but not executed. Also see RResultPtr.
2552    ///
2553    /// ### Example usage:
2554    /// ~~~{.cpp}
2555    /// auto myHist = myDf.Hist(10, {5, 15}, "col0");
2556    /// ~~~
2557    template <typename BinContentType = double, typename V = RDFDetail::RInferredType>
2558    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2559    Hist(std::uint64_t nNormalBins, std::pair<double, double> interval, std::string_view vName)
2560    {
2561       std::shared_ptr h = std::make_shared<ROOT::Experimental::RHist<BinContentType>>(nNormalBins, interval);
2562 
2563       const ColumnNames_t columnList = {std::string(vName)};
2564 
2565       return Hist<V>(h, columnList);
2566    }
2567 
2568    ////////////////////////////////////////////////////////////////////////////
2569    /// \brief Fill and return an RHist (*lazy action*).
2570    /// \tparam BinContentType The bin content type of the returned RHist.
2571    /// \param[in] axes The returned histogram will be constructed using these axes.
2572    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2573    /// \return the histogram wrapped in a RResultPtr.
2574    ///
2575    /// This action is *lazy*: upon invocation of this method the calculation is
2576    /// booked but not executed. Also see RResultPtr.
2577    ///
2578    /// ### Example usage:
2579    /// ~~~{.cpp}
2580    /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0});
2581    /// auto myHist = myDf.Hist({axis}, {"col0"});
2582    /// ~~~
2583    template <typename BinContentType = double, typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes>
2584    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2585    Hist(std::vector<ROOT::Experimental::RAxisVariant> axes, const ColumnNames_t &columnList)
2586    {
2587       if (axes.size() != columnList.size()) {
2588          std::string msg = "Wrong number of columns for the specified number of histogram axes: ";
2589          msg += "expected " + std::to_string(axes.size()) + ", got " + std::to_string(columnList.size());
2590          throw std::invalid_argument(msg);
2591       }
2592 
2593       std::shared_ptr h = std::make_shared<ROOT::Experimental::RHist<BinContentType>>(std::move(axes));
2594 
2595       return Hist<ColumnType, ColumnTypes...>(h, columnList);
2596    }
2597 
2598    ////////////////////////////////////////////////////////////////////////////
2599    /// \brief Fill the provided RHist (*lazy action*).
2600    /// \param[in] h The histogram that should be filled.
2601    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2602    /// \return the histogram wrapped in a RResultPtr.
2603    ///
2604    /// This action is *lazy*: upon invocation of this method the calculation is
2605    /// booked but not executed. Also see RResultPtr.
2606    ///
2607    /// During execution of the computation graph, the passed histogram must only be accessed with methods that are
2608    /// allowed during concurrent filling.
2609    ///
2610    /// ### Example usage:
2611    /// ~~~{.cpp}
2612    /// auto h = std::make_shared<ROOT::Experimental::RHist<double>>(10, {5.0, 15.0});
2613    /// auto myHist = myDf.Hist(h, {"col0"});
2614    /// ~~~
2615    template <typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes, typename BinContentType>
2616    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2617    Hist(std::shared_ptr<ROOT::Experimental::RHist<BinContentType>> h, const ColumnNames_t &columnList)
2618    {
2619       RDFInternal::WarnHist();
2620 
2621       if (h->GetNDimensions() != columnList.size()) {
2622          std::string msg = "Wrong number of columns for the passed histogram: ";
2623          msg += "expected " + std::to_string(h->GetNDimensions()) + ", got " + std::to_string(columnList.size());
2624          throw std::invalid_argument(msg);
2625       }
2626 
2627       return CreateAction<RDFInternal::ActionTags::Hist, ColumnType, ColumnTypes...>(columnList, h, h, fProxiedPtr,
2628                                                                                      columnList.size());
2629    }
2630 
2631    ////////////////////////////////////////////////////////////////////////////
2632    /// \brief Fill and return a one-dimensional RHist with weights (*lazy action*).
2633    /// \tparam BinContentType The bin content type of the returned RHist.
2634    /// \param[in] nNormalBins The returned histogram will be constructed using this number of normal bins.
2635    /// \param[in] interval The axis interval of the constructed histogram (lower end inclusive, upper end exclusive).
2636    /// \param[in] vName The name of the column that will fill the histogram.
2637    /// \param[in] wName The name of the column that will provide the weights.
2638    /// \return the histogram wrapped in a RResultPtr.
2639    ///
2640    /// This action is *lazy*: upon invocation of this method the calculation is
2641    /// booked but not executed. Also see RResultPtr.
2642    ///
2643    /// ### Example usage:
2644    /// ~~~{.cpp}
2645    /// auto myHist = myDf.Hist(10, {5, 15}, "col0", "colW");
2646    /// ~~~
2647    template <typename BinContentType = ROOT::Experimental::RBinWithError, typename V = RDFDetail::RInferredType,
2648              typename W = RDFDetail::RInferredType>
2649    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2650    Hist(std::uint64_t nNormalBins, std::pair<double, double> interval, std::string_view vName, std::string_view wName)
2651    {
2652       std::shared_ptr h = std::make_shared<ROOT::Experimental::RHist<BinContentType>>(nNormalBins, interval);
2653 
2654       const ColumnNames_t columnList = {std::string(vName)};
2655 
2656       return Hist<V, W>(h, columnList, wName);
2657    }
2658 
2659    ////////////////////////////////////////////////////////////////////////////
2660    /// \brief Fill and return an RHist with weights (*lazy action*).
2661    /// \tparam BinContentType The bin content type of the returned RHist.
2662    /// \param[in] axes The returned histogram will be constructed using these axes.
2663    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2664    /// \param[in] wName The name of the column that will provide the weights.
2665    /// \return the histogram wrapped in a RResultPtr.
2666    ///
2667    /// This action is *lazy*: upon invocation of this method the calculation is
2668    /// booked but not executed. Also see RResultPtr.
2669    ///
2670    /// This overload is not available for integral bin content types (see \ref RHistEngine::SupportsWeightedFilling).
2671    ///
2672    /// ### Example usage:
2673    /// ~~~{.cpp}
2674    /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0});
2675    /// auto myHist = myDf.Hist({axis}, {"col0"}, "colW");
2676    /// ~~~
2677    template <typename BinContentType = ROOT::Experimental::RBinWithError,
2678              typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes>
2679    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2680    Hist(std::vector<ROOT::Experimental::RAxisVariant> axes, const ColumnNames_t &columnList, std::string_view wName)
2681    {
2682       static_assert(ROOT::Experimental::RHistEngine<BinContentType>::SupportsWeightedFilling,
2683                     "weighted filling is not supported for integral bin content types");
2684 
2685       if (axes.size() != columnList.size()) {
2686          std::string msg = "Wrong number of columns for the specified number of histogram axes: ";
2687          msg += "expected " + std::to_string(axes.size()) + ", got " + std::to_string(columnList.size());
2688          throw std::invalid_argument(msg);
2689       }
2690 
2691       std::shared_ptr h = std::make_shared<ROOT::Experimental::RHist<BinContentType>>(std::move(axes));
2692 
2693       return Hist<ColumnType, ColumnTypes...>(h, columnList, wName);
2694    }
2695 
2696    ////////////////////////////////////////////////////////////////////////////
2697    /// \brief Fill the provided RHist with weights (*lazy action*).
2698    /// \param[in] h The histogram that should be filled.
2699    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2700    /// \param[in] wName The name of the column that will provide the weights.
2701    /// \return the histogram wrapped in a RResultPtr.
2702    ///
2703    /// This action is *lazy*: upon invocation of this method the calculation is
2704    /// booked but not executed. Also see RResultPtr.
2705    ///
2706    /// This overload is not available for integral bin content types (see \ref RHistEngine::SupportsWeightedFilling).
2707    ///
2708    /// During execution of the computation graph, the passed histogram must only be accessed with methods that are
2709    /// allowed during concurrent filling.
2710    ///
2711    /// ### Example usage:
2712    /// ~~~{.cpp}
2713    /// auto h = std::make_shared<ROOT::Experimental::RHist<double>>(10, {5.0, 15.0});
2714    /// auto myHist = myDf.Hist(h, {"col0"}, "colW");
2715    /// ~~~
2716    template <typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes, typename BinContentType>
2717    RResultPtr<ROOT::Experimental::RHist<BinContentType>>
2718    Hist(std::shared_ptr<ROOT::Experimental::RHist<BinContentType>> h, const ColumnNames_t &columnList,
2719         std::string_view wName)
2720    {
2721       static_assert(ROOT::Experimental::RHistEngine<BinContentType>::SupportsWeightedFilling,
2722                     "weighted filling is not supported for integral bin content types");
2723 
2724       RDFInternal::WarnHist();
2725 
2726       if (h->GetNDimensions() != columnList.size()) {
2727          std::string msg = "Wrong number of columns for the passed histogram: ";
2728          msg += "expected " + std::to_string(h->GetNDimensions()) + ", got " + std::to_string(columnList.size());
2729          throw std::invalid_argument(msg);
2730       }
2731 
2732       // Add the weight column to the list of argument columns to pass it through the infrastructure.
2733       ColumnNames_t columnListWithWeights(columnList);
2734       columnListWithWeights.push_back(std::string(wName));
2735 
2736       return CreateAction<RDFInternal::ActionTags::HistWithWeight, ColumnType, ColumnTypes...>(
2737          columnListWithWeights, h, h, fProxiedPtr, columnListWithWeights.size());
2738    }
2739 
2740    ////////////////////////////////////////////////////////////////////////////
2741    /// \brief Fill the provided RHistEngine (*lazy action*).
2742    /// \param[in] h The histogram that should be filled.
2743    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2744    /// \return the histogram wrapped in a RResultPtr.
2745    ///
2746    /// This action is *lazy*: upon invocation of this method the calculation is
2747    /// booked but not executed. Also see RResultPtr.
2748    ///
2749    /// During execution of the computation graph, the passed histogram must only be accessed with methods that are
2750    /// allowed during concurrent filling.
2751    ///
2752    /// ### Example usage:
2753    /// ~~~{.cpp}
2754    /// auto h = std::make_shared<ROOT::Experimental::RHistEngine<double>>(10, {5.0, 15.0});
2755    /// auto myHist = myDf.Hist(h, {"col0"});
2756    /// ~~~
2757    template <typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes, typename BinContentType>
2758    RResultPtr<ROOT::Experimental::RHistEngine<BinContentType>>
2759    Hist(std::shared_ptr<ROOT::Experimental::RHistEngine<BinContentType>> h, const ColumnNames_t &columnList)
2760    {
2761       RDFInternal::WarnHist();
2762 
2763       if (h->GetNDimensions() != columnList.size()) {
2764          std::string msg = "Wrong number of columns for the passed histogram: ";
2765          msg += "expected " + std::to_string(h->GetNDimensions()) + ", got " + std::to_string(columnList.size());
2766          throw std::invalid_argument(msg);
2767       }
2768 
2769       return CreateAction<RDFInternal::ActionTags::Hist, ColumnType, ColumnTypes...>(columnList, h, h, fProxiedPtr,
2770                                                                                      columnList.size());
2771    }
2772 
2773    ////////////////////////////////////////////////////////////////////////////
2774    /// \brief Fill the provided RHistEngine with weights (*lazy action*).
2775    /// \param[in] h The histogram that should be filled.
2776    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
2777    /// \param[in] wName The name of the column that will provide the weights.
2778    /// \return the histogram wrapped in a RResultPtr.
2779    ///
2780    /// This action is *lazy*: upon invocation of this method the calculation is
2781    /// booked but not executed. Also see RResultPtr.
2782    ///
2783    /// This overload is not available for integral bin content types (see \ref RHistEngine::SupportsWeightedFilling).
2784    ///
2785    /// During execution of the computation graph, the passed histogram must only be accessed with methods that are
2786    /// allowed during concurrent filling.
2787    ///
2788    /// ### Example usage:
2789    /// ~~~{.cpp}
2790    /// auto h = std::make_shared<ROOT::Experimental::RHistEngine<double>>(10, {5.0, 15.0});
2791    /// auto myHist = myDf.Hist(h, {"col0"}, "colW");
2792    /// ~~~
2793    template <typename ColumnType = RDFDetail::RInferredType, typename... ColumnTypes, typename BinContentType>
2794    RResultPtr<ROOT::Experimental::RHistEngine<BinContentType>>
2795    Hist(std::shared_ptr<ROOT::Experimental::RHistEngine<BinContentType>> h, const ColumnNames_t &columnList,
2796         std::string_view wName)
2797    {
2798       static_assert(ROOT::Experimental::RHistEngine<BinContentType>::SupportsWeightedFilling,
2799                     "weighted filling is not supported for integral bin content types");
2800 
2801       RDFInternal::WarnHist();
2802 
2803       if (h->GetNDimensions() != columnList.size()) {
2804          std::string msg = "Wrong number of columns for the passed histogram: ";
2805          msg += "expected " + std::to_string(h->GetNDimensions()) + ", got " + std::to_string(columnList.size());
2806          throw std::invalid_argument(msg);
2807       }
2808 
2809       // Add the weight column to the list of argument columns to pass it through the infrastructure.
2810       ColumnNames_t columnListWithWeights(columnList);
2811       columnListWithWeights.push_back(std::string(wName));
2812 
2813       return CreateAction<RDFInternal::ActionTags::HistWithWeight, ColumnType, ColumnTypes...>(
2814          columnListWithWeights, h, h, fProxiedPtr, columnListWithWeights.size());
2815    }
2816 #endif
2817 
2818    ////////////////////////////////////////////////////////////////////////////
2819    /// \brief Fill and return a TGraph object (*lazy action*).
2820    /// \tparam X The type of the column used to fill the x axis.
2821    /// \tparam Y The type of the column used to fill the y axis.
2822    /// \param[in] x The name of the column that will fill the x axis.
2823    /// \param[in] y The name of the column that will fill the y axis.
2824    /// \return the TGraph wrapped in a RResultPtr.
2825    ///
2826    /// Columns can be of a container type (e.g. std::vector<double>), in which case the TGraph
2827    /// is filled with each one of the elements of the container.
2828    /// If Multithreading is enabled, the order in which points are inserted is undefined.
2829    /// If the Graph has to be drawn, it is suggested to the user to sort it on the x before printing.
2830    /// A name and a title to the TGraph is given based on the input column names.
2831    ///
2832    /// This action is *lazy*: upon invocation of this method the calculation is
2833    /// booked but not executed. Also see RResultPtr.
2834    ///
2835    /// ### Example usage:
2836    /// ~~~{.cpp}
2837    /// // Deduce column types (this invocation needs jitting internally)
2838    /// auto myGraph1 = myDf.Graph("xValues", "yValues");
2839    /// // Explicit column types
2840    /// auto myGraph2 = myDf.Graph<int, float>("xValues", "yValues");
2841    /// ~~~
2842    ///
2843    /// \note Differently from other ROOT interfaces, the returned TGraph is not associated to gDirectory
2844    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
2845    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
2846    template <typename X = RDFDetail::RInferredType, typename Y = RDFDetail::RInferredType>
2847    RResultPtr<::TGraph> Graph(std::string_view x = "", std::string_view y = "")
2848    {
2849       auto graph = std::make_shared<::TGraph>();
2850       const std::vector<std::string_view> columnViews = {x, y};
2851       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2852                                   ? ColumnNames_t()
2853                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2854 
2855       const auto validatedColumns = GetValidatedColumnNames(2, userColumns);
2856 
2857       // We build a default name and title based on the input columns
2858       const auto g_name = validatedColumns[1] + "_vs_" + validatedColumns[0];
2859       const auto g_title = validatedColumns[1] + " vs " + validatedColumns[0];
2860       graph->SetNameTitle(g_name.c_str(), g_title.c_str());
2861       graph->GetXaxis()->SetTitle(validatedColumns[0].c_str());
2862       graph->GetYaxis()->SetTitle(validatedColumns[1].c_str());
2863 
2864       return CreateAction<RDFInternal::ActionTags::Graph, X, Y>(validatedColumns, graph, graph, fProxiedPtr);
2865    }
2866 
2867    ////////////////////////////////////////////////////////////////////////////
2868    /// \brief Fill and return a TGraphAsymmErrors object (*lazy action*).
2869    /// \param[in] x The name of the column that will fill the x axis.
2870    /// \param[in] y The name of the column that will fill the y axis.
2871    /// \param[in] exl The name of the column of X low errors
2872    /// \param[in] exh The name of the column of X high errors
2873    /// \param[in] eyl The name of the column of Y low errors
2874    /// \param[in] eyh The name of the column of Y high errors
2875    /// \return the TGraphAsymmErrors wrapped in a RResultPtr.
2876    ///
2877    /// Columns can be of a container type (e.g. std::vector<double>), in which case the graph
2878    /// is filled with each one of the elements of the container.
2879    /// If Multithreading is enabled, the order in which points are inserted is undefined.
2880    ///
2881    /// This action is *lazy*: upon invocation of this method the calculation is
2882    /// booked but not executed. Also see RResultPtr.
2883    ///
2884    /// ### Example usage:
2885    /// ~~~{.cpp}
2886    /// // Deduce column types (this invocation needs jitting internally)
2887    /// auto myGAE1 = myDf.GraphAsymmErrors("xValues", "yValues", "exl", "exh", "eyl", "eyh");
2888    /// // Explicit column types
2889    /// using f = float
2890    /// auto myGAE2 = myDf.GraphAsymmErrors<f, f, f, f, f, f>("xValues", "yValues", "exl", "exh", "eyl", "eyh");
2891    /// ~~~
2892    ///
2893    /// `GraphAsymmErrors` should also be used for the cases in which values associated only with
2894    /// one of the axes have associated errors. For example, only `ey` exist and `ex` are equal to zero.
2895    /// In such cases, user should do the following:
2896    /// ~~~{.cpp}
2897    /// // Create a column of zeros in RDataFrame
2898    /// auto rdf_withzeros = rdf.Define("zero", "0");
2899    /// // or alternatively:
2900    /// auto rdf_withzeros = rdf.Define("zero", []() -> double { return 0.;});
2901    /// // Create the graph with y errors only
2902    /// auto rdf_errorsOnYOnly = rdf_withzeros.GraphAsymmErrors("xValues", "yValues", "zero", "zero", "eyl", "eyh");
2903    /// ~~~
2904    ///
2905    /// \note Differently from other ROOT interfaces, the returned TGraphAsymmErrors is not associated to gDirectory
2906    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
2907    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
2908    template <typename X = RDFDetail::RInferredType, typename Y = RDFDetail::RInferredType,
2909              typename EXL = RDFDetail::RInferredType, typename EXH = RDFDetail::RInferredType,
2910              typename EYL = RDFDetail::RInferredType, typename EYH = RDFDetail::RInferredType>
2911    RResultPtr<::TGraphAsymmErrors>
2912    GraphAsymmErrors(std::string_view x = "", std::string_view y = "", std::string_view exl = "",
2913                     std::string_view exh = "", std::string_view eyl = "", std::string_view eyh = "")
2914    {
2915       auto graph = std::make_shared<::TGraphAsymmErrors>();
2916       const std::vector<std::string_view> columnViews = {x, y, exl, exh, eyl, eyh};
2917       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2918                                   ? ColumnNames_t()
2919                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2920 
2921       const auto validatedColumns = GetValidatedColumnNames(6, userColumns);
2922 
2923       // We build a default name and title based on the input columns
2924       const auto g_name = validatedColumns[1] + "_vs_" + validatedColumns[0];
2925       const auto g_title = validatedColumns[1] + " vs " + validatedColumns[0];
2926       graph->SetNameTitle(g_name.c_str(), g_title.c_str());
2927       graph->GetXaxis()->SetTitle(validatedColumns[0].c_str());
2928       graph->GetYaxis()->SetTitle(validatedColumns[1].c_str());
2929 
2930       return CreateAction<RDFInternal::ActionTags::GraphAsymmErrors, X, Y, EXL, EXH, EYL, EYH>(validatedColumns, graph,
2931                                                                                                graph, fProxiedPtr);
2932    }
2933 
2934    ////////////////////////////////////////////////////////////////////////////
2935    /// \brief Fill and return a one-dimensional profile (*lazy action*).
2936    /// \tparam V1 The type of the column the values of which are used to fill the profile. Inferred if not present.
2937    /// \tparam V2 The type of the column the values of which are used to fill the profile. Inferred if not present.
2938    /// \param[in] model The model to be considered to build the new return value.
2939    /// \param[in] v1Name The name of the column that will fill the x axis.
2940    /// \param[in] v2Name The name of the column that will fill the y axis.
2941    /// \return the monodimensional profile wrapped in a RResultPtr.
2942    ///
2943    /// This action is *lazy*: upon invocation of this method the calculation is
2944    /// booked but not executed. Also see RResultPtr.
2945    ///
2946    /// ### Example usage:
2947    /// ~~~{.cpp}
2948    /// // Deduce column types (this invocation needs jitting internally)
2949    /// auto myProf1 = myDf.Profile1D({"profName", "profTitle", 64u, -4., 4.}, "xValues", "yValues");
2950    /// // Explicit column types
2951    /// auto myProf2 = myDf.Graph<int, float>({"profName", "profTitle", 64u, -4., 4.}, "xValues", "yValues");
2952    /// ~~~
2953    ///
2954    /// \note Differently from other ROOT interfaces, the returned profile is not associated to gDirectory
2955    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
2956    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
2957    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType>
2958    RResultPtr<::TProfile>
2959    Profile1D(const TProfile1DModel &model, std::string_view v1Name = "", std::string_view v2Name = "")
2960    {
2961       std::shared_ptr<::TProfile> h(nullptr);
2962       {
2963          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
2964          h = model.GetProfile();
2965       }
2966 
2967       if (!RDFInternal::HistoUtils<::TProfile>::HasAxisLimits(*h)) {
2968          throw std::runtime_error("Profiles with no axes limits are not supported yet.");
2969       }
2970       const std::vector<std::string_view> columnViews = {v1Name, v2Name};
2971       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
2972                                   ? ColumnNames_t()
2973                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
2974       return CreateAction<RDFInternal::ActionTags::Profile1D, V1, V2>(userColumns, h, h, fProxiedPtr);
2975    }
2976 
2977    ////////////////////////////////////////////////////////////////////////////
2978    /// \brief Fill and return a one-dimensional profile (*lazy action*).
2979    /// \tparam V1 The type of the column the values of which are used to fill the profile. Inferred if not present.
2980    /// \tparam V2 The type of the column the values of which are used to fill the profile. Inferred if not present.
2981    /// \tparam W The type of the column the weights of which are used to fill the profile. Inferred if not present.
2982    /// \param[in] model The model to be considered to build the new return value.
2983    /// \param[in] v1Name The name of the column that will fill the x axis.
2984    /// \param[in] v2Name The name of the column that will fill the y axis.
2985    /// \param[in] wName The name of the column that will provide the weights.
2986    /// \return the monodimensional profile wrapped in a RResultPtr.
2987    ///
2988    /// This action is *lazy*: upon invocation of this method the calculation is
2989    /// booked but not executed. Also see RResultPtr.
2990    ///
2991    /// ### Example usage:
2992    /// ~~~{.cpp}
2993    /// // Deduce column types (this invocation needs jitting internally)
2994    /// auto myProf1 = myDf.Profile1D({"profName", "profTitle", 64u, -4., 4.}, "xValues", "yValues", "weight");
2995    /// // Explicit column types
2996    /// auto myProf2 = myDf.Profile1D<int, float, double>({"profName", "profTitle", 64u, -4., 4.},
2997    ///                                                   "xValues", "yValues", "weight");
2998    /// ~~~
2999    ///
3000    /// See the first Profile1D() overload for more details.
3001    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
3002              typename W = RDFDetail::RInferredType>
3003    RResultPtr<::TProfile>
3004    Profile1D(const TProfile1DModel &model, std::string_view v1Name, std::string_view v2Name, std::string_view wName)
3005    {
3006       std::shared_ptr<::TProfile> h(nullptr);
3007       {
3008          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
3009          h = model.GetProfile();
3010       }
3011 
3012       if (!RDFInternal::HistoUtils<::TProfile>::HasAxisLimits(*h)) {
3013          throw std::runtime_error("Profile histograms with no axes limits are not supported yet.");
3014       }
3015       const std::vector<std::string_view> columnViews = {v1Name, v2Name, wName};
3016       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
3017                                   ? ColumnNames_t()
3018                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
3019       return CreateAction<RDFInternal::ActionTags::Profile1D, V1, V2, W>(userColumns, h, h, fProxiedPtr);
3020    }
3021 
3022    ////////////////////////////////////////////////////////////////////////////
3023    /// \brief Fill and return a one-dimensional profile (*lazy action*).
3024    /// See the first Profile1D() overload for more details.
3025    template <typename V1, typename V2, typename W>
3026    RResultPtr<::TProfile> Profile1D(const TProfile1DModel &model)
3027    {
3028       return Profile1D<V1, V2, W>(model, "", "", "");
3029    }
3030 
3031    ////////////////////////////////////////////////////////////////////////////
3032    /// \brief Fill and return a two-dimensional profile (*lazy action*).
3033    /// \tparam V1 The type of the column used to fill the x axis of the histogram. Inferred if not present.
3034    /// \tparam V2 The type of the column used to fill the y axis of the histogram. Inferred if not present.
3035    /// \tparam V3 The type of the column used to fill the z axis of the histogram. Inferred if not present.
3036    /// \param[in] model The returned profile will be constructed using this as a model.
3037    /// \param[in] v1Name The name of the column that will fill the x axis.
3038    /// \param[in] v2Name The name of the column that will fill the y axis.
3039    /// \param[in] v3Name The name of the column that will fill the z axis.
3040    /// \return the bidimensional profile wrapped in a RResultPtr.
3041    ///
3042    /// This action is *lazy*: upon invocation of this method the calculation is
3043    /// booked but not executed. Also see RResultPtr.
3044    ///
3045    /// ### Example usage:
3046    /// ~~~{.cpp}
3047    /// // Deduce column types (this invocation needs jitting internally)
3048    /// auto myProf1 = myDf.Profile2D({"profName", "profTitle", 40, -4, 4, 40, -4, 4, 0, 20},
3049    ///                               "xValues", "yValues", "zValues");
3050    /// // Explicit column types
3051    /// auto myProf2 = myDf.Profile2D<int, float, double>({"profName", "profTitle", 40, -4, 4, 40, -4, 4, 0, 20},
3052    ///                                                   "xValues", "yValues", "zValues");
3053    /// ~~~
3054    ///
3055    /// \note Differently from other ROOT interfaces, the returned profile is not associated to gDirectory
3056    /// and the caller is responsible for its lifetime (in particular, a typical source of confusion is that
3057    /// if result histograms go out of scope before the end of the program, ROOT might display a blank canvas).
3058    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
3059              typename V3 = RDFDetail::RInferredType>
3060    RResultPtr<::TProfile2D> Profile2D(const TProfile2DModel &model, std::string_view v1Name = "",
3061                                       std::string_view v2Name = "", std::string_view v3Name = "")
3062    {
3063       std::shared_ptr<::TProfile2D> h(nullptr);
3064       {
3065          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
3066          h = model.GetProfile();
3067       }
3068 
3069       if (!RDFInternal::HistoUtils<::TProfile2D>::HasAxisLimits(*h)) {
3070          throw std::runtime_error("2D profiles with no axes limits are not supported yet.");
3071       }
3072       const std::vector<std::string_view> columnViews = {v1Name, v2Name, v3Name};
3073       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
3074                                   ? ColumnNames_t()
3075                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
3076       return CreateAction<RDFInternal::ActionTags::Profile2D, V1, V2, V3>(userColumns, h, h, fProxiedPtr);
3077    }
3078 
3079    ////////////////////////////////////////////////////////////////////////////
3080    /// \brief Fill and return a two-dimensional profile (*lazy action*).
3081    /// \tparam V1 The type of the column used to fill the x axis of the histogram. Inferred if not present.
3082    /// \tparam V2 The type of the column used to fill the y axis of the histogram. Inferred if not present.
3083    /// \tparam V3 The type of the column used to fill the z axis of the histogram. Inferred if not present.
3084    /// \tparam W The type of the column used for the weights of the histogram. Inferred if not present.
3085    /// \param[in] model The returned histogram will be constructed using this as a model.
3086    /// \param[in] v1Name The name of the column that will fill the x axis.
3087    /// \param[in] v2Name The name of the column that will fill the y axis.
3088    /// \param[in] v3Name The name of the column that will fill the z axis.
3089    /// \param[in] wName The name of the column that will provide the weights.
3090    /// \return the bidimensional profile wrapped in a RResultPtr.
3091    ///
3092    /// This action is *lazy*: upon invocation of this method the calculation is
3093    /// booked but not executed. Also see RResultPtr.
3094    ///
3095    /// ### Example usage:
3096    /// ~~~{.cpp}
3097    /// // Deduce column types (this invocation needs jitting internally)
3098    /// auto myProf1 = myDf.Profile2D({"profName", "profTitle", 40, -4, 4, 40, -4, 4, 0, 20},
3099    ///                               "xValues", "yValues", "zValues", "weight");
3100    /// // Explicit column types
3101    /// auto myProf2 = myDf.Profile2D<int, float, double, int>({"profName", "profTitle", 40, -4, 4, 40, -4, 4, 0, 20},
3102    ///                                                        "xValues", "yValues", "zValues", "weight");
3103    /// ~~~
3104    ///
3105    /// See the first Profile2D() overload for more details.
3106    template <typename V1 = RDFDetail::RInferredType, typename V2 = RDFDetail::RInferredType,
3107              typename V3 = RDFDetail::RInferredType, typename W = RDFDetail::RInferredType>
3108    RResultPtr<::TProfile2D> Profile2D(const TProfile2DModel &model, std::string_view v1Name, std::string_view v2Name,
3109                                       std::string_view v3Name, std::string_view wName)
3110    {
3111       std::shared_ptr<::TProfile2D> h(nullptr);
3112       {
3113          ROOT::Internal::RDF::RIgnoreErrorLevelRAII iel(kError);
3114          h = model.GetProfile();
3115       }
3116 
3117       if (!RDFInternal::HistoUtils<::TProfile2D>::HasAxisLimits(*h)) {
3118          throw std::runtime_error("2D profiles with no axes limits are not supported yet.");
3119       }
3120       const std::vector<std::string_view> columnViews = {v1Name, v2Name, v3Name, wName};
3121       const auto userColumns = RDFInternal::AtLeastOneEmptyString(columnViews)
3122                                   ? ColumnNames_t()
3123                                   : ColumnNames_t(columnViews.begin(), columnViews.end());
3124       return CreateAction<RDFInternal::ActionTags::Profile2D, V1, V2, V3, W>(userColumns, h, h, fProxiedPtr);
3125    }
3126 
3127    /// \brief Fill and return a two-dimensional profile (*lazy action*).
3128    /// See the first Profile2D() overload for more details.
3129    template <typename V1, typename V2, typename V3, typename W>
3130    RResultPtr<::TProfile2D> Profile2D(const TProfile2DModel &model)
3131    {
3132       return Profile2D<V1, V2, V3, W>(model, "", "", "", "");
3133    }
3134 
3135    ////////////////////////////////////////////////////////////////////////////
3136    /// \brief Return an object of type T on which `T::Fill` will be called once per event (*lazy action*).
3137    ///
3138    /// Type T must provide at least:
3139    /// - a copy-constructor
3140    /// - a `Fill` method that accepts as many arguments and with same types as the column names passed as columnList
3141    ///   (these types can also be passed as template parameters to this method)
3142    /// - a `Merge` method with signature `Merge(TCollection *)` or `Merge(const std::vector<T *>&)` that merges the
3143    ///   objects passed as argument into the object on which `Merge` was called (an analogous of TH1::Merge). Note that
3144    ///   if the signature that takes a `TCollection*` is used, then T must inherit from TObject (to allow insertion in
3145    ///   the TCollection*).
3146    ///
3147    /// \tparam FirstColumn The first type of the column the values of which are used to fill the object. Inferred together with OtherColumns if not present.
3148    /// \tparam OtherColumns A list of the other types of the columns the values of which are used to fill the object.
3149    /// \tparam T The type of the object to fill. Automatically deduced.
3150    /// \param[in] model The model to be considered to build the new return value.
3151    /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill`
3152    /// \return the filled object wrapped in a RResultPtr.
3153    ///
3154    /// The user gives up ownership of the model object.
3155    /// The list of column names to be used for filling must always be specified.
3156    /// This action is *lazy*: upon invocation of this method the calculation is booked but not executed.
3157    /// Also see RResultPtr.
3158    ///
3159    /// ### Example usage:
3160    /// ~~~{.cpp}
3161    /// MyClass obj;
3162    /// // Deduce column types (this invocation needs jitting internally, and in this case
3163    /// // MyClass needs to be known to the interpreter)
3164    /// auto myFilledObj = myDf.Fill(obj, {"col0", "col1"});
3165    /// // explicit column types
3166    /// auto myFilledObj = myDf.Fill<float, float>(obj, {"col0", "col1"});
3167    /// ~~~
3168    ///
3169    template <typename FirstColumn = RDFDetail::RInferredType, typename... OtherColumns, typename T>
3170    RResultPtr<std::decay_t<T>> Fill(T &&model, const ColumnNames_t &columnList)
3171    {
3172       auto h = std::make_shared<std::decay_t<T>>(std::forward<T>(model));
3173       if (!RDFInternal::HistoUtils<T>::HasAxisLimits(*h)) {
3174          throw std::runtime_error("The absence of axes limits is not supported yet.");
3175       }
3176       return CreateAction<RDFInternal::ActionTags::Fill, FirstColumn, OtherColumns...>(columnList, h, h, fProxiedPtr,
3177                                                                                        columnList.size());
3178    }
3179 
3180    ////////////////////////////////////////////////////////////////////////////
3181    /// \brief Return a TStatistic object, filled once per event (*lazy action*).
3182    ///
3183    /// \tparam V The type of the value column
3184    /// \param[in] value The name of the column with the values to fill the statistics with.
3185    /// \return the filled TStatistic object wrapped in a RResultPtr.
3186    ///
3187    /// ### Example usage:
3188    /// ~~~{.cpp}
3189    /// // Deduce column type (this invocation needs jitting internally)
3190    /// auto stats0 = myDf.Stats("values");
3191    /// // Explicit column type
3192    /// auto stats1 = myDf.Stats<float>("values");
3193    /// ~~~
3194    ///
3195    template <typename V = RDFDetail::RInferredType>
3196    RResultPtr<TStatistic> Stats(std::string_view value = "")
3197    {
3198       ColumnNames_t columns;
3199       if (!value.empty()) {
3200          columns.emplace_back(std::string(value));
3201       }
3202       const auto validColumnNames = GetValidatedColumnNames(1, columns);
3203       if (std::is_same<V, RDFDetail::RInferredType>::value) {
3204          return Fill(TStatistic(), validColumnNames);
3205       } else {
3206          return Fill<V>(TStatistic(), validColumnNames);
3207       }
3208    }
3209 
3210    ////////////////////////////////////////////////////////////////////////////
3211    /// \brief Return a TStatistic object, filled once per event (*lazy action*).
3212    ///
3213    /// \tparam V The type of the value column
3214    /// \tparam W The type of the weight column
3215    /// \param[in] value The name of the column with the values to fill the statistics with.
3216    /// \param[in] weight The name of the column with the weights to fill the statistics with.
3217    /// \return the filled TStatistic object wrapped in a RResultPtr.
3218    ///
3219    /// ### Example usage:
3220    /// ~~~{.cpp}
3221    /// // Deduce column types (this invocation needs jitting internally)
3222    /// auto stats0 = myDf.Stats("values", "weights");
3223    /// // Explicit column types
3224    /// auto stats1 = myDf.Stats<int, float>("values", "weights");
3225    /// ~~~
3226    ///
3227    template <typename V = RDFDetail::RInferredType, typename W = RDFDetail::RInferredType>
3228    RResultPtr<TStatistic> Stats(std::string_view value, std::string_view weight)
3229    {
3230       ColumnNames_t columns{std::string(value), std::string(weight)};
3231       constexpr auto vIsInferred = std::is_same<V, RDFDetail::RInferredType>::value;
3232       constexpr auto wIsInferred = std::is_same<W, RDFDetail::RInferredType>::value;
3233       const auto validColumnNames = GetValidatedColumnNames(2, columns);
3234       // We have 3 cases:
3235       // 1. Both types are inferred: we use Fill and let the jit kick in.
3236       // 2. One of the two types is explicit and the other one is inferred: the case is not supported.
3237       // 3. Both types are explicit: we invoke the fully compiled Fill method.
3238       if (vIsInferred && wIsInferred) {
3239          return Fill(TStatistic(), validColumnNames);
3240       } else if (vIsInferred != wIsInferred) {
3241          std::string error("The ");
3242          error += vIsInferred ? "value " : "weight ";
3243          error += "column type is explicit, while the ";
3244          error += vIsInferred ? "weight " : "value ";
3245          error += " is specified to be inferred. This case is not supported: please specify both types or none.";
3246          throw std::runtime_error(error);
3247       } else {
3248          return Fill<V, W>(TStatistic(), validColumnNames);
3249       }
3250    }
3251 
3252    ////////////////////////////////////////////////////////////////////////////
3253    /// \brief Return the minimum of processed column values (*lazy action*).
3254    /// \tparam T The type of the branch/column.
3255    /// \param[in] columnName The name of the branch/column to be treated.
3256    /// \return the minimum value of the selected column wrapped in a RResultPtr.
3257    ///
3258    /// If T is not specified, RDataFrame will infer it from the data and just-in-time compile the correct
3259    /// template specialization of this method.
3260    /// If the type of the column is inferred, the return type is `double`, the type of the column otherwise.
3261    ///
3262    /// This action is *lazy*: upon invocation of this method the calculation is
3263    /// booked but not executed. Also see RResultPtr.
3264    ///
3265    /// ### Example usage:
3266    /// ~~~{.cpp}
3267    /// // Deduce column type (this invocation needs jitting internally)
3268    /// auto minVal0 = myDf.Min("values");
3269    /// // Explicit column type
3270    /// auto minVal1 = myDf.Min<double>("values");
3271    /// ~~~
3272    ///
3273    template <typename T = RDFDetail::RInferredType>
3274    RResultPtr<RDFDetail::MinReturnType_t<T>> Min(std::string_view columnName = "")
3275    {
3276       const auto userColumns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3277       using RetType_t = RDFDetail::MinReturnType_t<T>;
3278       auto minV = std::make_shared<RetType_t>(std::numeric_limits<RetType_t>::max());
3279       return CreateAction<RDFInternal::ActionTags::Min, T>(userColumns, minV, minV, fProxiedPtr);
3280    }
3281 
3282    ////////////////////////////////////////////////////////////////////////////
3283    /// \brief Return the maximum of processed column values (*lazy action*).
3284    /// \tparam T The type of the branch/column.
3285    /// \param[in] columnName The name of the branch/column to be treated.
3286    /// \return the maximum value of the selected column wrapped in a RResultPtr.
3287    ///
3288    /// If T is not specified, RDataFrame will infer it from the data and just-in-time compile the correct
3289    /// template specialization of this method.
3290    /// If the type of the column is inferred, the return type is `double`, the type of the column otherwise.
3291    ///
3292    /// This action is *lazy*: upon invocation of this method the calculation is
3293    /// booked but not executed. Also see RResultPtr.
3294    ///
3295    /// ### Example usage:
3296    /// ~~~{.cpp}
3297    /// // Deduce column type (this invocation needs jitting internally)
3298    /// auto maxVal0 = myDf.Max("values");
3299    /// // Explicit column type
3300    /// auto maxVal1 = myDf.Max<double>("values");
3301    /// ~~~
3302    ///
3303    template <typename T = RDFDetail::RInferredType>
3304    RResultPtr<RDFDetail::MaxReturnType_t<T>> Max(std::string_view columnName = "")
3305    {
3306       const auto userColumns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3307       using RetType_t = RDFDetail::MaxReturnType_t<T>;
3308       auto maxV = std::make_shared<RetType_t>(std::numeric_limits<RetType_t>::lowest());
3309       return CreateAction<RDFInternal::ActionTags::Max, T>(userColumns, maxV, maxV, fProxiedPtr);
3310    }
3311 
3312    ////////////////////////////////////////////////////////////////////////////
3313    /// \brief Return the mean of processed column values (*lazy action*).
3314    /// \tparam T The type of the branch/column.
3315    /// \param[in] columnName The name of the branch/column to be treated.
3316    /// \return the mean value of the selected column wrapped in a RResultPtr.
3317    ///
3318    /// If T is not specified, RDataFrame will infer it from the data and just-in-time compile the correct
3319    /// template specialization of this method.
3320    /// Note that internally, the summations are executed with Kahan sums in double precision, irrespective
3321    /// of the type of column that is read.
3322    ///
3323    /// This action is *lazy*: upon invocation of this method the calculation is
3324    /// booked but not executed. Also see RResultPtr.
3325    ///
3326    /// ### Example usage:
3327    /// ~~~{.cpp}
3328    /// // Deduce column type (this invocation needs jitting internally)
3329    /// auto meanVal0 = myDf.Mean("values");
3330    /// // Explicit column type
3331    /// auto meanVal1 = myDf.Mean<double>("values");
3332    /// ~~~
3333    ///
3334    template <typename T = RDFDetail::RInferredType>
3335    RResultPtr<double> Mean(std::string_view columnName = "")
3336    {
3337       const auto userColumns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3338       auto meanV = std::make_shared<double>(0);
3339       return CreateAction<RDFInternal::ActionTags::Mean, T>(userColumns, meanV, meanV, fProxiedPtr);
3340    }
3341 
3342    ////////////////////////////////////////////////////////////////////////////
3343    /// \brief Return the unbiased standard deviation of processed column values (*lazy action*).
3344    /// \tparam T The type of the branch/column.
3345    /// \param[in] columnName The name of the branch/column to be treated.
3346    /// \return the standard deviation value of the selected column wrapped in a RResultPtr.
3347    ///
3348    /// If T is not specified, RDataFrame will infer it from the data and just-in-time compile the correct
3349    /// template specialization of this method.
3350    ///
3351    /// This action is *lazy*: upon invocation of this method the calculation is
3352    /// booked but not executed. Also see RResultPtr.
3353    ///
3354    /// ### Example usage:
3355    /// ~~~{.cpp}
3356    /// // Deduce column type (this invocation needs jitting internally)
3357    /// auto stdDev0 = myDf.StdDev("values");
3358    /// // Explicit column type
3359    /// auto stdDev1 = myDf.StdDev<double>("values");
3360    /// ~~~
3361    ///
3362    template <typename T = RDFDetail::RInferredType>
3363    RResultPtr<double> StdDev(std::string_view columnName = "")
3364    {
3365       const auto userColumns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3366       auto stdDeviationV = std::make_shared<double>(0);
3367       return CreateAction<RDFInternal::ActionTags::StdDev, T>(userColumns, stdDeviationV, stdDeviationV, fProxiedPtr);
3368    }
3369 
3370    // clang-format off
3371    ////////////////////////////////////////////////////////////////////////////
3372    /// \brief Return the sum of processed column values (*lazy action*).
3373    /// \tparam T The type of the branch/column.
3374    /// \param[in] columnName The name of the branch/column.
3375    /// \param[in] initValue Optional initial value for the sum. If not present, the column values must be default-constructible.
3376    /// \return the sum of the selected column wrapped in a RResultPtr.
3377    ///
3378    /// If T is not specified, RDataFrame will infer it from the data and just-in-time compile the correct
3379    /// template specialization of this method.
3380    /// If the type of the column is inferred, the return type is `double`, the type of the column otherwise.
3381    ///
3382    /// This action is *lazy*: upon invocation of this method the calculation is
3383    /// booked but not executed. Also see RResultPtr.
3384    ///
3385    /// ### Example usage:
3386    /// ~~~{.cpp}
3387    /// // Deduce column type (this invocation needs jitting internally)
3388    /// auto sum0 = myDf.Sum("values");
3389    /// // Explicit column type
3390    /// auto sum1 = myDf.Sum<double>("values");
3391    /// ~~~
3392    ///
3393    template <typename T = RDFDetail::RInferredType>
3394    RResultPtr<RDFDetail::SumReturnType_t<T>>
3395    Sum(std::string_view columnName = "",
3396        const RDFDetail::SumReturnType_t<T> &initValue = RDFDetail::SumReturnType_t<T>{})
3397    {
3398       const auto userColumns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3399       auto sumV = std::make_shared<RDFDetail::SumReturnType_t<T>>(initValue);
3400       return CreateAction<RDFInternal::ActionTags::Sum, T>(userColumns, sumV, sumV, fProxiedPtr);
3401    }
3402    // clang-format on
3403 
3404    ////////////////////////////////////////////////////////////////////////////
3405    /// \brief Gather filtering statistics.
3406    /// \return the resulting `RCutFlowReport` instance wrapped in a RResultPtr.
3407    ///
3408    /// Calling `Report` on the main `RDataFrame` object gathers stats for
3409    /// all named filters in the call graph. Calling this method on a
3410    /// stored chain state (i.e. a graph node different from the first) gathers
3411    /// the stats for all named filters in the chain section between the original
3412    /// `RDataFrame` and that node (included). Stats are gathered in the same
3413    /// order as the named filters have been added to the graph.
3414    /// A RResultPtr<RCutFlowReport> is returned to allow inspection of the
3415    /// effects cuts had.
3416    ///
3417    /// This action is *lazy*: upon invocation of
3418    /// this method the calculation is booked but not executed. See RResultPtr
3419    /// documentation.
3420    ///
3421    /// ### Example usage:
3422    /// ~~~{.cpp}
3423    /// auto filtered = d.Filter(cut1, {"b1"}, "Cut1").Filter(cut2, {"b2"}, "Cut2");
3424    /// auto cutReport = filtered3.Report();
3425    /// cutReport->Print();
3426    /// ~~~
3427    ///
3428    RResultPtr<RCutFlowReport> Report()
3429    {
3430       bool returnEmptyReport = false;
3431       // if this is a RInterface<RLoopManager> on which `Define` has been called, users
3432       // are calling `Report` on a chain of the form LoopManager->Define->Define->..., which
3433       // certainly does not contain named filters.
3434       // The number 4 takes into account the implicit columns for entry and slot number
3435       // and their aliases (2 + 2, i.e. {r,t}dfentry_ and {r,t}dfslot_)
3436       if (std::is_same<Proxied, RLoopManager>::value && fColRegister.GenerateColumnNames().size() > 4)
3437          returnEmptyReport = true;
3438 
3439       auto rep = std::make_shared<RCutFlowReport>();
3440       using Helper_t = RDFInternal::ReportHelper<Proxied>;
3441       using Action_t = RDFInternal::RAction<Helper_t, Proxied>;
3442 
3443       auto action = std::make_unique<Action_t>(Helper_t(rep, fProxiedPtr.get(), returnEmptyReport), ColumnNames_t({}),
3444                                                fProxiedPtr, RDFInternal::RColumnRegister(fColRegister));
3445 
3446       return MakeResultPtr(rep, *fLoopManager, std::move(action));
3447    }
3448 
3449    /// \brief Returns the names of the filters created.
3450    /// \return the container of filters names.
3451    ///
3452    /// If called on a root node, all the filters in the computation graph will
3453    /// be printed. For any other node, only the filters upstream of that node.
3454    /// Filters without a name are printed as "Unnamed Filter"
3455    /// This is not an action nor a transformation, just a query to the RDataFrame object.
3456    ///
3457    /// ### Example usage:
3458    /// ~~~{.cpp}
3459    /// auto filtNames = d.GetFilterNames();
3460    /// for (auto &&filtName : filtNames) std::cout << filtName << std::endl;
3461    /// ~~~
3462    ///
3463    std::vector<std::string> GetFilterNames() { return RDFInternal::GetFilterNames(fProxiedPtr); }
3464 
3465    // clang-format off
3466    ////////////////////////////////////////////////////////////////////////////
3467    /// \brief Execute a user-defined accumulation operation on the processed column values in each processing slot.
3468    /// \tparam F The type of the aggregator callable. Automatically deduced.
3469    /// \tparam U The type of the aggregator variable. Must be default-constructible, copy-constructible and copy-assignable. Automatically deduced.
3470    /// \tparam T The type of the column to apply the reduction to. Automatically deduced.
3471    /// \param[in] aggregator A callable with signature `U(U,T)` or `void(U&,T)`, where T is the type of the column, U is the type of the aggregator variable
3472    /// \param[in] merger A callable with signature `U(U,U)` or `void(std::vector<U>&)` used to merge the results of the accumulations of each thread
3473    /// \param[in] columnName The column to be aggregated. If omitted, the first default column is used instead.
3474    /// \param[in] aggIdentity The aggregator variable of each thread is initialized to this value (or is default-constructed if the parameter is omitted)
3475    /// \return the result of the aggregation wrapped in a RResultPtr.
3476    ///
3477    /// An aggregator callable takes two values, an aggregator variable and a column value. The aggregator variable is
3478    /// initialized to aggIdentity or default-constructed if aggIdentity is omitted.
3479    /// This action calls the aggregator callable for each processed entry, passing in the aggregator variable and
3480    /// the value of the column columnName.
3481    /// If the signature is `U(U,T)` the aggregator variable is then copy-assigned the result of the execution of the callable.
3482    /// Otherwise the signature of aggregator must be `void(U&,T)`.
3483    ///
3484    /// The merger callable is used to merge the partial accumulation results of each processing thread. It is only called in multi-thread executions.
3485    /// If its signature is `U(U,U)` the aggregator variables of each thread are merged two by two.
3486    /// If its signature is `void(std::vector<U>& a)` it is assumed that it merges all aggregators in a[0].
3487    ///
3488    /// This action is *lazy*: upon invocation of this method the calculation is booked but not executed. Also see RResultPtr.
3489    ///
3490    /// Example usage:
3491    /// ~~~{.cpp}
3492    /// auto aggregator = [](double acc, double x) { return acc * x; };
3493    /// ROOT::EnableImplicitMT();
3494    /// // If multithread is enabled, the aggregator function will be called by more threads
3495    /// // and will produce a vector of partial accumulators.
3496    /// // The merger function performs the final aggregation of these partial results.
3497    /// auto merger = [](std::vector<double> &accumulators) {
3498    ///    for (auto i : ROOT::TSeqU(1u, accumulators.size())) {
3499    ///       accumulators[0] *= accumulators[i];
3500    ///    }
3501    /// };
3502    ///
3503    /// // The accumulator is initialized at this value by every thread.
3504    /// double initValue = 1.;
3505    ///
3506    /// // Multiplies all elements of the column "x"
3507    /// auto result = d.Aggregate(aggregator, merger, "x", initValue);
3508    /// ~~~
3509    // clang-format on
3510    template <typename AccFun, typename MergeFun, typename R = typename TTraits::CallableTraits<AccFun>::ret_type,
3511              typename ArgTypes = typename TTraits::CallableTraits<AccFun>::arg_types,
3512              typename ArgTypesNoDecay = typename TTraits::CallableTraits<AccFun>::arg_types_nodecay,
3513              typename U = TTraits::TakeFirstParameter_t<ArgTypes>,
3514              typename T = TTraits::TakeFirstParameter_t<TTraits::RemoveFirstParameter_t<ArgTypes>>>
3515    RResultPtr<U> Aggregate(AccFun aggregator, MergeFun merger, std::string_view columnName, const U &aggIdentity)
3516    {
3517       RDFInternal::CheckAggregate<R, MergeFun>(ArgTypesNoDecay());
3518       const auto columns = columnName.empty() ? ColumnNames_t() : ColumnNames_t({std::string(columnName)});
3519 
3520       const auto validColumnNames = GetValidatedColumnNames(1, columns);
3521       CheckAndFillDSColumns(validColumnNames, TTraits::TypeList<T>());
3522 
3523       auto accObjPtr = std::make_shared<U>(aggIdentity);
3524       using Helper_t = RDFInternal::AggregateHelper<AccFun, MergeFun, R, T, U>;
3525       using Action_t = RDFInternal::RAction<Helper_t, Proxied>;
3526       auto action = std::make_unique<Action_t>(
3527          Helper_t(std::move(aggregator), std::move(merger), accObjPtr, fLoopManager->GetNSlots()), validColumnNames,
3528          fProxiedPtr, fColRegister);
3529       return MakeResultPtr(accObjPtr, *fLoopManager, std::move(action));
3530    }
3531 
3532    // clang-format off
3533    ////////////////////////////////////////////////////////////////////////////
3534    /// \brief Execute a user-defined accumulation operation on the processed column values in each processing slot.
3535    /// \tparam F The type of the aggregator callable. Automatically deduced.
3536    /// \tparam U The type of the aggregator variable. Must be default-constructible, copy-constructible and copy-assignable. Automatically deduced.
3537    /// \tparam T The type of the column to apply the reduction to. Automatically deduced.
3538    /// \param[in] aggregator A callable with signature `U(U,T)` or `void(U,T)`, where T is the type of the column, U is the type of the aggregator variable
3539    /// \param[in] merger A callable with signature `U(U,U)` or `void(std::vector<U>&)` used to merge the results of the accumulations of each thread
3540    /// \param[in] columnName The column to be aggregated. If omitted, the first default column is used instead.
3541    /// \return the result of the aggregation wrapped in a RResultPtr.
3542    ///
3543    /// See previous Aggregate overload for more information.
3544    // clang-format on
3545    template <typename AccFun, typename MergeFun, typename R = typename TTraits::CallableTraits<AccFun>::ret_type,
3546              typename ArgTypes = typename TTraits::CallableTraits<AccFun>::arg_types,
3547              typename U = TTraits::TakeFirstParameter_t<ArgTypes>,
3548              typename T = TTraits::TakeFirstParameter_t<TTraits::RemoveFirstParameter_t<ArgTypes>>>
3549    RResultPtr<U> Aggregate(AccFun aggregator, MergeFun merger, std::string_view columnName = "")
3550    {
3551       static_assert(
3552          std::is_default_constructible<U>::value,
3553          "aggregated object cannot be default-constructed. Please provide an initialisation value (aggIdentity)");
3554       return Aggregate(std::move(aggregator), std::move(merger), columnName, U());
3555    }
3556 
3557    // clang-format off
3558    ////////////////////////////////////////////////////////////////////////////
3559    /// \brief Book execution of a custom action using a user-defined helper object.
3560    /// \tparam FirstColumn The type of the first column used by this action.  Inferred together with OtherColumns if not present.
3561    /// \tparam OtherColumns A list of the types of the other columns used by this action
3562    /// \tparam Helper The type of the user-defined helper. See below for the required interface it should expose.
3563    /// \param[in] helper The Action Helper to be scheduled.
3564    /// \param[in] columns The names of the columns on which the helper acts.
3565    /// \return the result of the helper wrapped in a RResultPtr.
3566    ///
3567    /// This method books a custom action for execution. The behavior of the action is completely dependent on the
3568    /// Helper object provided by the caller. The required interface for the helper is described below (more
3569    /// methods that the ones required can be present, e.g. a constructor that takes the number of worker threads is usually useful):
3570    ///
3571    /// ### Mandatory interface
3572    ///
3573    /// * `Helper` must publicly inherit from `ROOT::Detail::RDF::RActionImpl<Helper>`
3574    /// * `Helper::Result_t`: public alias for the type of the result of this action helper. `Result_t` must be default-constructible.
3575    /// * `Helper(Helper &&)`: a move-constructor is required. Copy-constructors are discouraged.
3576    /// * `std::shared_ptr<Result_t> GetResultPtr() const`: return a shared_ptr to the result of this action (of type
3577    ///   Result_t). The RResultPtr returned by Book will point to this object. Note that this method can be called
3578    ///   _before_ Initialize(), because the RResultPtr is constructed before the event loop is started.
3579    /// * `void Initialize()`: this method is called once before starting the event-loop. Useful for setup operations.
3580    ///   It must reset the state of the helper to the expected state at the beginning of the event loop: the same helper,
3581    ///   or copies of it, might be used for multiple event loops (e.g. in the presence of systematic variations).
3582    /// * `void InitTask(TTreeReader *, unsigned int slot)`: each working thread shall call this method during the event
3583    ///   loop, before processing a batch of entries. The pointer passed as argument, if not null, will point to the TTreeReader
3584    ///   that RDataFrame has set up to read the task's batch of entries. It is passed to the helper to allow certain advanced optimizations
3585    ///   it should not usually serve any purpose for the Helper. This method is often no-op for simple helpers.
3586    /// * `void Exec(unsigned int slot, ColumnTypes...columnValues)`: each working thread shall call this method
3587    ///   during the event-loop, possibly concurrently. No two threads will ever call Exec with the same 'slot' value:
3588    ///   this parameter is there to facilitate writing thread-safe helpers. The other arguments will be the values of
3589    ///   the requested columns for the particular entry being processed.
3590    /// * `void Finalize()`: this method is called at the end of the event loop. Commonly used to finalize the contents of the result.
3591    /// * `std::string GetActionName()`: it returns a string identifier for this type of action that RDataFrame will use in
3592    ///    diagnostics, SaveGraph(), etc.
3593    ///
3594    /// ### Optional methods
3595    ///
3596    /// If these methods are implemented they enable extra functionality as per the description below.
3597    ///
3598    /// * `Result_t &PartialUpdate(unsigned int slot)`: if present, it must return the value of the partial result of this action for the given 'slot'.
3599    ///   Different threads might call this method concurrently, but will do so with different 'slot' numbers.
3600    ///   RDataFrame leverages this method to implement RResultPtr::OnPartialResult().
3601    /// * `ROOT::RDF::SampleCallback_t GetSampleCallback()`: if present, it must return a callable with the
3602    ///   appropriate signature (see ROOT::RDF::SampleCallback_t) that will be invoked at the beginning of the processing
3603    ///   of every sample, as in DefinePerSample().
3604    /// * `Helper MakeNew(void *newResult, std::string_view variation = "nominal")`: if implemented, it enables varying
3605    ///   the action's result with VariationsFor(). It takes a type-erased new result that can be safely cast to a
3606    ///   `std::shared_ptr<Result_t> *` (a pointer to shared pointer) and should be used as the action's output result.
3607    ///   The function optionally takes the name of the current variation which could be useful in customizing its behaviour.
3608    ///
3609    /// In case Book is called without specifying column types as template arguments, corresponding typed code will be just-in-time compiled
3610    /// by RDataFrame. In that case the Helper class needs to be known to the ROOT interpreter.
3611    ///
3612    /// This action is *lazy*: upon invocation of this method the calculation is booked but not executed. Also see RResultPtr.
3613    ///
3614    /// ### Examples
3615    /// See [this tutorial](https://root.cern/doc/master/df018__customActions_8C.html) for an example implementation of an action helper.
3616    ///
3617    /// It is also possible to inspect the code used by built-in RDataFrame actions at ActionHelpers.hxx.
3618    ///
3619    // clang-format on
3620    template <typename FirstColumn = RDFDetail::RInferredType, typename... OtherColumns, typename Helper>
3621    RResultPtr<typename std::decay_t<Helper>::Result_t> Book(Helper &&helper, const ColumnNames_t &columns = {})
3622    {
3623       using HelperT = std::decay_t<Helper>;
3624       // TODO add more static sanity checks on Helper
3625       using AH = RDFDetail::RActionImpl<HelperT>;
3626       static_assert(std::is_base_of<AH, HelperT>::value && std::is_convertible<HelperT *, AH *>::value,
3627                     "Action helper of type T must publicly inherit from ROOT::Detail::RDF::RActionImpl<T>");
3628 
3629       auto hPtr = std::make_shared<HelperT>(std::forward<Helper>(helper));
3630       auto resPtr = hPtr->GetResultPtr();
3631 
3632       if (std::is_same<FirstColumn, RDFDetail::RInferredType>::value && columns.empty()) {
3633          return CallCreateActionWithoutColsIfPossible<HelperT>(resPtr, hPtr, TTraits::TypeList<FirstColumn>{});
3634       } else {
3635          return CreateAction<RDFInternal::ActionTags::Book, FirstColumn, OtherColumns...>(columns, resPtr, hPtr,
3636                                                                                           fProxiedPtr, columns.size());
3637       }
3638    }
3639 
3640    ////////////////////////////////////////////////////////////////////////////
3641    /// \brief Provides a representation of the columns in the dataset.
3642    /// \tparam ColumnTypes variadic list of branch/column types.
3643    /// \param[in] columnList Names of the columns to be displayed.
3644    /// \param[in] nRows Number of events for each column to be displayed.
3645    /// \param[in] nMaxCollectionElements Maximum number of collection elements to display per row.
3646    /// \return the `RDisplay` instance wrapped in a RResultPtr.
3647    ///
3648    /// This function returns a `RResultPtr<RDisplay>` containing all the entries to be displayed, organized in a tabular
3649    /// form. RDisplay will either print on the standard output a summarized version through `RDisplay::Print()` or will
3650    /// return a complete version through `RDisplay::AsString()`.
3651    ///
3652    /// This action is *lazy*: upon invocation of this method the calculation is booked but not executed. Also see
3653    /// RResultPtr.
3654    ///
3655    /// Example usage:
3656    /// ~~~{.cpp}
3657    /// // Preparing the RResultPtr<RDisplay> object with all columns and default number of entries
3658    /// auto d1 = rdf.Display("");
3659    /// // Preparing the RResultPtr<RDisplay> object with two columns and 128 entries
3660    /// auto d2 = d.Display({"x", "y"}, 128);
3661    /// // Printing the short representations, the event loop will run
3662    /// d1->Print();
3663    /// d2->Print();
3664    /// ~~~
3665    template <typename... ColumnTypes>
3666    RResultPtr<RDisplay> Display(const ColumnNames_t &columnList, size_t nRows = 5, size_t nMaxCollectionElements = 10)
3667    {
3668       CheckIMTDisabled("Display");
3669       auto newCols = columnList;
3670       newCols.insert(newCols.begin(), "rdfentry_"); // Artificially insert first column
3671       auto displayer = std::make_shared<RDisplay>(newCols, GetColumnTypeNamesList(newCols), nMaxCollectionElements);
3672       using displayHelperArgs_t = std::pair<size_t, std::shared_ptr<RDisplay>>;
3673       // Need to add ULong64_t type corresponding to the first column rdfentry_
3674       return CreateAction<RDFInternal::ActionTags::Display, ULong64_t, ColumnTypes...>(
3675          std::move(newCols), displayer, std::make_shared<displayHelperArgs_t>(nRows, displayer), fProxiedPtr);
3676    }
3677 
3678    ////////////////////////////////////////////////////////////////////////////
3679    /// \brief Provides a representation of the columns in the dataset.
3680    /// \param[in] columnList Names of the columns to be displayed.
3681    /// \param[in] nRows Number of events for each column to be displayed.
3682    /// \param[in] nMaxCollectionElements  Maximum number of collection elements to display per row.
3683    /// \return the `RDisplay` instance wrapped in a RResultPtr.
3684    ///
3685    /// This overload automatically infers the column types.
3686    /// See the previous overloads for further details.
3687    ///
3688    /// Invoked when no types are specified to Display
3689    RResultPtr<RDisplay> Display(const ColumnNames_t &columnList, size_t nRows = 5, size_t nMaxCollectionElements = 10)
3690    {
3691       CheckIMTDisabled("Display");
3692       auto newCols = columnList;
3693       newCols.insert(newCols.begin(), "rdfentry_"); // Artificially insert first column
3694       auto displayer = std::make_shared<RDisplay>(newCols, GetColumnTypeNamesList(newCols), nMaxCollectionElements);
3695       using displayHelperArgs_t = std::pair<size_t, std::shared_ptr<RDisplay>>;
3696       return CreateAction<RDFInternal::ActionTags::Display, RDFDetail::RInferredType>(
3697          std::move(newCols), displayer, std::make_shared<displayHelperArgs_t>(nRows, displayer), fProxiedPtr,
3698          columnList.size() + 1);
3699    }
3700 
3701    ////////////////////////////////////////////////////////////////////////////
3702    /// \brief Provides a representation of the columns in the dataset.
3703    /// \param[in] columnNameRegexp A regular expression to select the columns.
3704    /// \param[in] nRows Number of events for each column to be displayed.
3705    /// \param[in] nMaxCollectionElements Maximum number of collection elements to display per row.
3706    /// \return the `RDisplay` instance wrapped in a RResultPtr.
3707    ///
3708    /// The existing columns are matched against the regular expression. If the string provided
3709    /// is empty, all columns are selected.
3710    /// See the previous overloads for further details.
3711    RResultPtr<RDisplay>
3712    Display(std::string_view columnNameRegexp = "", size_t nRows = 5, size_t nMaxCollectionElements = 10)
3713    {
3714       const auto columnNames = GetColumnNames();
3715       const auto selectedColumns = RDFInternal::ConvertRegexToColumns(columnNames, columnNameRegexp, "Display");
3716       return Display(selectedColumns, nRows, nMaxCollectionElements);
3717    }
3718 
3719    ////////////////////////////////////////////////////////////////////////////
3720    /// \brief Provides a representation of the columns in the dataset.
3721    /// \param[in] columnList Names of the columns to be displayed.
3722    /// \param[in] nRows Number of events for each column to be displayed.
3723    /// \param[in] nMaxCollectionElements Number of maximum elements in collection.
3724    /// \return the `RDisplay` instance wrapped in a RResultPtr.
3725    ///
3726    /// See the previous overloads for further details.
3727    RResultPtr<RDisplay>
3728    Display(std::initializer_list<std::string> columnList, size_t nRows = 5, size_t nMaxCollectionElements = 10)
3729    {
3730       ColumnNames_t selectedColumns(columnList);
3731       return Display(selectedColumns, nRows, nMaxCollectionElements);
3732    }
3733 
3734 private:
3735    template <typename F, typename DefineType, typename RetType = typename TTraits::CallableTraits<F>::ret_type>
3736    std::enable_if_t<std::is_default_constructible<RetType>::value, RInterface<Proxied>>
3737    DefineImpl(std::string_view name, F &&expression, const ColumnNames_t &columns, const std::string &where)
3738    {
3739       if (where.compare(0, 8, "Redefine") != 0) { // not a Redefine
3740          RDFInternal::CheckValidCppVarName(name, where);
3741          RDFInternal::CheckForRedefinition(where, name, fColRegister,
3742                                            GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
3743       } else {
3744          RDFInternal::CheckForDefinition(where, name, fColRegister,
3745                                          GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
3746          RDFInternal::CheckForNoVariations(where, name, fColRegister);
3747       }
3748 
3749       using ArgTypes_t = typename TTraits::CallableTraits<F>::arg_types;
3750       using ColTypesTmp_t = typename RDFInternal::RemoveFirstParameterIf<
3751          std::is_same<DefineType, RDFDetail::ExtraArgsForDefine::Slot>::value, ArgTypes_t>::type;
3752       using ColTypes_t = typename RDFInternal::RemoveFirstTwoParametersIf<
3753          std::is_same<DefineType, RDFDetail::ExtraArgsForDefine::SlotAndEntry>::value, ColTypesTmp_t>::type;
3754 
3755       constexpr auto nColumns = ColTypes_t::list_size;
3756 
3757       const auto validColumnNames = GetValidatedColumnNames(nColumns, columns);
3758       CheckAndFillDSColumns(validColumnNames, ColTypes_t());
3759 
3760       // Declare return type to the interpreter, for future use by jitted actions
3761       auto retTypeName = RDFInternal::TypeID2TypeName(typeid(RetType));
3762       if (retTypeName.empty()) {
3763          // The type is not known to the interpreter.
3764          // We must not error out here, but if/when this column is used in jitted code
3765          const auto demangledType = RDFInternal::DemangleTypeIdName(typeid(RetType));
3766          retTypeName = "CLING_UNKNOWN_TYPE_" + demangledType;
3767       }
3768 
3769       using NewCol_t = RDFDetail::RDefine<F, DefineType>;
3770       auto newColumn = std::make_shared<NewCol_t>(name, retTypeName, std::forward<F>(expression), validColumnNames,
3771                                                   fColRegister, *fLoopManager);
3772 
3773       RDFInternal::RColumnRegister newCols(fColRegister);
3774       newCols.AddDefine(std::move(newColumn));
3775 
3776       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
3777 
3778       return newInterface;
3779    }
3780 
3781    // This overload is chosen when the callable passed to Define or DefineSlot returns void.
3782    // It simply fires a compile-time error. This is preferable to a static_assert in the main `Define` overload because
3783    // this way compilation of `Define` has no way to continue after throwing the error.
3784    template <typename F, typename DefineType, typename RetType = typename TTraits::CallableTraits<F>::ret_type,
3785              bool IsFStringConv = std::is_convertible<F, std::string>::value,
3786              bool IsRetTypeDefConstr = std::is_default_constructible<RetType>::value>
3787    std::enable_if_t<!IsFStringConv && !IsRetTypeDefConstr, RInterface<Proxied>>
3788    DefineImpl(std::string_view, F, const ColumnNames_t &, const std::string &)
3789    {
3790       static_assert(std::is_default_constructible<typename TTraits::CallableTraits<F>::ret_type>::value,
3791                     "Error in `Define`: type returned by expression is not default-constructible");
3792       return *this; // never reached
3793    }
3794 
3795    ////////////////////////////////////////////////////////////////////////////
3796    /// \brief Implementation of cache.
3797    template <typename... ColTypes, std::size_t... S>
3798    RInterface<RLoopManager> CacheImpl(const ColumnNames_t &columnList, std::index_sequence<S...>)
3799    {
3800       const auto columnListWithoutSizeColumns = RDFInternal::FilterArraySizeColNames(columnList, "Snapshot");
3801 
3802       // Check at compile time that the columns types are copy constructible
3803       constexpr bool areCopyConstructible =
3804          RDFInternal::TEvalAnd<std::is_copy_constructible<ColTypes>::value...>::value;
3805       static_assert(areCopyConstructible, "Columns of a type which is not copy constructible cannot be cached yet.");
3806 
3807       RDFInternal::CheckTypesAndPars(sizeof...(ColTypes), columnListWithoutSizeColumns.size());
3808 
3809       auto colHolders = std::make_tuple(Take<ColTypes>(columnListWithoutSizeColumns[S])...);
3810       auto ds = std::make_unique<RLazyDS<ColTypes...>>(
3811          std::make_pair(columnListWithoutSizeColumns[S], std::get<S>(colHolders))...);
3812 
3813       RInterface<RLoopManager> cachedRDF(std::make_shared<RLoopManager>(std::move(ds), columnListWithoutSizeColumns));
3814 
3815       return cachedRDF;
3816    }
3817 
3818    template <bool IsSingleColumn, typename F>
3819    RInterface<Proxied>
3820    VaryImpl(const std::vector<std::string> &colNames, F &&expression, const ColumnNames_t &inputColumns,
3821             const std::vector<std::string> &variationTags, std::string_view variationName)
3822    {
3823       using F_t = std::decay_t<F>;
3824       using ColTypes_t = typename TTraits::CallableTraits<F_t>::arg_types;
3825       using RetType = typename TTraits::CallableTraits<F_t>::ret_type;
3826       constexpr auto nColumns = ColTypes_t::list_size;
3827 
3828       SanityChecksForVary<RetType>(colNames, variationTags, variationName);
3829 
3830       const auto validColumnNames = GetValidatedColumnNames(nColumns, inputColumns);
3831       CheckAndFillDSColumns(validColumnNames, ColTypes_t{});
3832 
3833       auto retTypeName = RDFInternal::TypeID2TypeName(typeid(RetType));
3834       if (retTypeName.empty()) {
3835          // The type is not known to the interpreter, but we don't want to error out
3836          // here, rather if/when this column is used in jitted code, so we inject a broken but telling type name.
3837          const auto demangledType = RDFInternal::DemangleTypeIdName(typeid(RetType));
3838          retTypeName = "CLING_UNKNOWN_TYPE_" + demangledType;
3839       }
3840 
3841       auto variation = std::make_shared<RDFInternal::RVariation<F_t, IsSingleColumn>>(
3842          colNames, variationName, std::forward<F>(expression), variationTags, retTypeName, fColRegister, *fLoopManager,
3843          validColumnNames);
3844 
3845       RDFInternal::RColumnRegister newCols(fColRegister);
3846       newCols.AddVariation(std::move(variation));
3847 
3848       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newCols));
3849 
3850       return newInterface;
3851    }
3852 
3853    RInterface<Proxied> JittedVaryImpl(const std::vector<std::string> &colNames, std::string_view expression,
3854                                       const std::vector<std::string> &variationTags, std::string_view variationName,
3855                                       bool isSingleColumn)
3856    {
3857       R__ASSERT(!variationTags.empty() && "Must have at least one variation.");
3858       R__ASSERT(!colNames.empty() && "Must have at least one varied column.");
3859       R__ASSERT(!variationName.empty() && "Must provide a variation name.");
3860 
3861       for (auto &colName : colNames) {
3862          RDFInternal::CheckValidCppVarName(colName, "Vary");
3863          RDFInternal::CheckForDefinition("Vary", colName, fColRegister,
3864                                          GetDataSource() ? GetDataSource()->GetColumnNames() : ColumnNames_t{});
3865       }
3866       RDFInternal::CheckValidCppVarName(variationName, "Vary");
3867 
3868       // when varying multiple columns, they must be different columns
3869       if (colNames.size() > 1) {
3870          std::set<std::string> uniqueCols(colNames.begin(), colNames.end());
3871          if (uniqueCols.size() != colNames.size())
3872             throw std::logic_error("A column name was passed to the same Vary invocation multiple times.");
3873       }
3874 
3875       // Cannot vary different input column types, assume the first
3876       auto varyColType = GetColumnType(colNames[0]);
3877       auto jittedVariation =
3878          RDFInternal::BookVariationJit(colNames, variationName, variationTags, expression, *fLoopManager,
3879                                        GetDataSource(), fColRegister, isSingleColumn, varyColType);
3880 
3881       RDFInternal::RColumnRegister newColRegister(fColRegister);
3882       newColRegister.AddVariation(std::move(jittedVariation));
3883 
3884       RInterface<Proxied> newInterface(fProxiedPtr, *fLoopManager, std::move(newColRegister));
3885 
3886       return newInterface;
3887    }
3888 
3889    template <typename Helper, typename ActionResultType>
3890    auto CallCreateActionWithoutColsIfPossible(const std::shared_ptr<ActionResultType> &resPtr,
3891                                               const std::shared_ptr<Helper> &hPtr,
3892                                               TTraits::TypeList<RDFDetail::RInferredType>)
3893       -> decltype(hPtr->Exec(0u), RResultPtr<ActionResultType>{})
3894    {
3895       return CreateAction<RDFInternal::ActionTags::Book>(/*columns=*/{}, resPtr, hPtr, fProxiedPtr, 0u);
3896    }
3897 
3898    template <typename Helper, typename ActionResultType, typename... Others>
3899    RResultPtr<ActionResultType>
3900    CallCreateActionWithoutColsIfPossible(const std::shared_ptr<ActionResultType> &,
3901                                          const std::shared_ptr<Helper>& /*hPtr*/,
3902                                          Others...)
3903    {
3904       throw std::logic_error(std::string("An action was booked with no input columns, but the action requires "
3905                                          "columns! The action helper type was ") +
3906                              typeid(Helper).name());
3907       return {};
3908    }
3909 
3910 protected:
3911    RInterface(const std::shared_ptr<Proxied> &proxied, RLoopManager &lm,
3912               const RDFInternal::RColumnRegister &colRegister)
3913       : RInterfaceBase(lm, colRegister), fProxiedPtr(proxied)
3914    {
3915    }
3916 
3917    const std::shared_ptr<Proxied> &GetProxiedPtr() const { return fProxiedPtr; }
3918 };
3919 
3920 } // namespace RDF
3921 
3922 } // namespace ROOT
3923 
3924 #endif // ROOT_RDF_INTERFACE