c++testingtuplesconstexprcompile-time

C++: How to iterate over tuple in compile-time?


How to iterate over tuple in compile-time?

problem code:

#include <array>
#include <cstddef>
#include <tuple>

namespace {

  class Solution {
  public:
    template <size_t sizeTV> [[nodiscard]] consteval int maxArea(
        std::array<int, sizeTV> const& /*height*/) const {
      return 1;
    }
  };

  template <size_t sizeTV> struct TestStruct {
    std::array<int, sizeTV> numbers;
    int expected_result;
  };

}  // namespace

int main() {
  constexpr auto data_test = std::make_tuple(
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
      TestStruct{.numbers = std::to_array({1, 2, 3}), .expected_result = 2});

   // for element in tuple:
      {

          constexpr Solution functor;

          static_assert(functor.maxArea(element.numbers)
                        == element.expected_result);
      }
}

Manually iterating without tuple (works) https://godbolt.org/z/TMcbETTPv :

  constexpr TestStruct test_data1{.numbers = std::to_array({1, 1}),
                                  .expected_result = 1};
  constexpr TestStruct test_data2{.numbers = std::to_array({1, 2, 3}),
                                  .expected_result = 2};

  constexpr Solution functor;

  static_assert(functor.maxArea(test_data1.numbers)
                == test_data1.expected_result);

  static_assert(functor.maxArea(test_data2.numbers)
                == test_data2.expected_result);

With tuple manually iterating (works) https://godbolt.org/z/b3Wrqo3GY :

int main() {
  constexpr auto data_test = std::make_tuple(
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
      TestStruct{.numbers = std::to_array({1, 2, 3}), .expected_result = 2});

  constexpr Solution functor;

  static_assert(functor.maxArea(std::get<0>(data_test).numbers)
                == std::get<0>(data_test).expected_result);
  static_assert(functor.maxArea(std::get<1>(data_test).numbers)
                == std::get<1>(data_test).expected_result);
}

With tuple iterate via std::apply ( does not work ) https://godbolt.org/z/vaa673b6a:

  std::apply(
      [](auto const& test_struct) {
        constexpr Solution functor;

        static_assert(functor.maxArea(test_struct.numbers)
                      == test_struct.expected_result);
      },
      data_test);

boost::mp11 tuple_for_each(f, tp) ( does not work ):

  boost::mp11::tuple_for_each(data_test, [](auto const& test_struct) {
    constexpr solution functor;

    static_assert(functor.maxarea(test_struct.numbers)
                  == test_struct.expected_result);
  });

Manually writing for_each via std::index_sequence: ( does not work ) https://godbolt.org/z/5j46eqjWr

  template <size_t sizeTV>
  consteval void test_with_TestStruct(TestStruct<sizeTV>const & test_data) {
    constexpr Solution functor;

    static_assert(functor.maxArea(test_data.numbers)
                  == test_data.expected_result);
  }

  template <typename Tuple, size_t... I>
  consteval void test_impl(Tuple tuple_to_apply_at,
                           std::index_sequence<I...> /*unused*/) {
    (test_with_TestStruct(std::get<I>(tuple_to_apply_at)), ...);
  }

  template <typename Tuple> consteval auto test(Tuple tuple_to_apply_at) {
    constexpr auto size = std::tuple_size<Tuple>::value;
    test_impl(tuple_to_apply_at, std::make_index_sequence<size>{});
  }

int main() {
  constexpr auto data_test = std::make_tuple(
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
      TestStruct{.numbers = std::to_array({1, 2, 3}), .expected_result = 2});

  test(data_test);
}

Via a custom parameter packing (does not work) https://godbolt.org/z/W8oTP34sd :

template <std::size_t I = 0, size_t ... SizeTV>
constexpr void test(std::tuple<TestStruct<SizeTV>...> tup) {
  if constexpr (I == sizeof...(SizeTV)) {
    return;
  } else {
    constexpr Solution functor;

    static_assert(functor.maxArea(std::get<I>(tup).numbers) == std::get<I>(tup).expected_result);
    test<I+1>(tup);
  }
}

int main() {
  constexpr auto data_test = std::make_tuple(
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
      TestStruct{.numbers = std::to_array({1, 2, 3}), .expected_result = 2});

   test(data_test);
}

Via boost::fusion::for_each (doesn't work) ( note: candidate template ignored: couldn't infer template argument 'F' , because IDK how to give it template function, which seems on edge of impossible):

  template <size_t sizeTV>
  constexpr void test_with_TestStruct(TestStruct<sizeTV> test_data) {
    constexpr Solution functor;

    static_assert(functor.maxArea(test_data.numbers)
                  == test_data.expected_result);
  }

int main() {

  constexpr auto data_test = std::make_tuple(
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
      TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1});

   boost::fusion::for_each(data_test,test_with_TestStruct);

}


Solution

  • See template "ForEach" and "ForEachTupleType" below (the latter relies on the former), which I assume you can figure out (they're relatively short - see function "main" for an example that targets your specific code). These are generic templates that are internal to my FunctionTraits library ("ForEachTupleType" generically iterates any "std::tuple"), but stripped down here to reduce them to minimal, more easily digestible versions for you (these templates drive template ForEachArg in the library itself, where the latter internally targets a "std::tuple" of all arg types in an arbitrary function using the above templates - "ForEachTupleType" can be used to target any tuple however).

    The code below therefore targets C++20 or later only, which your code apparently supports (it's relying on "std::to_array" for instance). To this end, I've removed the library's code that targets C++17 (since it doesn't support lambda templates - the stripped down code below does). Other things have also been removed in order to boil it all down for you (so all comments for these templates in the library have been removed below since they are quite lengthy, and all concepts to enforce template args have also been removed. - all these things will likely just get in your way for learning purposes but you can consult the library's source itself if you wish to see the complete, commented versions).

    Note that both "For" templates below are (effectively) designed as recursive routines which won't result in major code bloat for tuples that aren't excessively large (which in practice is normally the case - if you're targeting potentially large tuples you may wish to reconsider this recursive design).

    You can loop through any arbitrary tuple with "ForEachTupleType" and even exit on any given iteration if you don't need to process all elements (similar to a "break" statement in a regular "for" loop - the code below demos this). I haven't examined your particular code in detail however but you can either use the code below as-is or leverage the basic design for your specific needs (the code below is just to demo the basic usage of template "ForEachTupleType").

    Click here to run it

    #include <array>
    #include <cstddef>
    #include <tuple>
    #include <utility>
    
    /////////////////////////////////////////////////////
    // Generic template that invokes arg "functor" (a
    // template-based functor) "N" times unless "functor"
    // returns false to stop iterating, similar to a
    // "break" statement in a regular "for" loop
    /////////////////////////////////////////////////////
    template <std::size_t N,
              typename ForEachFunctorT>
    inline constexpr bool ForEach(ForEachFunctorT&& functor)
    {
        const auto forEachImpl = [&functor]<std::size_t I>
                                 (const auto &forEachImpl)
                                 {
                                     if constexpr (I < N)
                                     {
                                         if (std::forward<ForEachFunctorT>(functor).template operator()<I>())
                                         {
                                             return forEachImpl.template operator()<I + 1>(forEachImpl);
                                         }
                                         else
                                         {
                                             return false;
                                         }
                                     }
                                     else
                                     {
                                         return true;
                                     }
                                 };
    
        return forEachImpl.template operator()<0>(forEachImpl);
    }
    
    //////////////////////////////////////////////////////
    // Generic template that invokes arg "functor" (a
    // template-based functor) once for each tuple type
    // in "TupleT" unless "functor returns false to
    // stop iterating, similar to a "break" statement
    // in a regular "for" loop
    //////////////////////////////////////////////////////
    template <typename TupleT,
              typename ForEachTupleTypeFunctorT>
    inline constexpr bool ForEachTupleType(ForEachTupleTypeFunctorT&& functor)
    {
        if constexpr (std::tuple_size_v<TupleT> != 0)
        {
            const auto processTupleType = [&functor]<std::size_t I>()
                                          {
                                              using TupleElement_t = std::tuple_element_t<I, TupleT>;
                                              
                                              return std::forward<ForEachTupleTypeFunctorT>(functor).template operator()<I, TupleElement_t>();
                                          };
            return ForEach<std::tuple_size_v<TupleT>>(processTupleType);
        }
        else
        {
            return true;
        }
    }      
    
    // Op's original code
    namespace {
    
      class Solution {
      public:
        template <size_t sizeTV> [[nodiscard]] consteval int maxArea(
            std::array<int, sizeTV> const& /*height*/) const {
          return 1;
        }
      };
    
      template <size_t sizeTV> struct TestStruct {
        std::array<int, sizeTV> numbers;
        int expected_result;
      };
    }
    
    int main()
    {
        // Op's original code
        constexpr Solution functor;
        constexpr auto data_test = std::make_tuple(
          TestStruct{.numbers = std::to_array({1, 1}), .expected_result = 1},
          TestStruct{.numbers = std::to_array({1, 2, 3}), .expected_result = 2});
    
        // See comments in code that follows
        std::size_t Failed_I;
    
        /////////////////////////////////////////
        // Lambda template that will be invoked
        // just below for each type in tuple
        // "data_test" (template "I" is the
        // zero-based index on each iteration,
        // and "T" is its type in the tuple)
        /////////////////////////////////////////
        const auto processTupleType = [&]<std::size_t I, typename T>()
                                      {
                                          ///////////////////////////////////////////////////
                                          // I just quickly glossed over this in your code,
                                          // so it's presumably correct (you can adjust it
                                          // as required if not, it's not relevant for the
                                          // purposes of this example, to demonstrate how
                                          // to iterate a tuple using a recursive design)
                                          ///////////////////////////////////////////////////
                                          if constexpr (functor.maxArea(std::get<I>(data_test).numbers)
                                                        == std::get<I>(data_test).expected_result)
                                          {
                                              // Continue iterating
                                              return true;
                                          }
                                          else
                                          {
                                              // Available on exit (see comments just below)
                                              Failed_I = I;
    
                                              ///////////////////////////////////////////////////
                                              // Stop iterating, similar to a "break" statement
                                              // in a regular "for" loop. "Failed_I" is then
                                              // available on exit. In fact, this can be more
                                              // cleanly encapsulated than I'm doing here,
                                              // e.g,, you can replace this lambda template with
                                              // a traditional (non-lambda template) functor
                                              // instead and encapsulate "Failed_I" as a member
                                              // of your functor. The "operator()" member of your
                                              // functor should be a template however, identical
                                              // to this lambda template so it needs to be a
                                              // template with "std::size_t I" and "typename T"
                                              // template args (just like this lambda template is
                                              // behind the scenes).
                                              ///////////////////////////////////////////////////
                                              return false;
                                          }
                                      };
    
        /////////////////////////////////////////////////
        // Iterate each type in "data_test", invoking
        // lambda template "processTupleType" above for
        // each (returning true or false from above
        // lambda template accordingly - see comments
        // in lambda template above)
        /////////////////////////////////////////////////
        if (!ForEachTupleType<decltype(data_test)>(processTupleType))
        {
            /////////////////////////////////////////////////
            // "Failed_I" is now set (remains uninitialized
            // however if you don't come through here so
            // caution advised)
            /////////////////////////////////////////////////
        }
    
        return 0;
    }