1

I want to write an abstract Pipeline class that gets some functions with a specific signature as template parameters and runs them depending on the parse stage for input data. this is the code that I wrote:

#include <array>

enum class ParseState
{
  EXTRACTED,   /**< EXTRACTED */
  IN_PROGRESS, /**< IN_PROGRESS */
  REJECTED,    /**< REJECTED */
};

struct ParseStage
{
  std::size_t stage;

public:
  ParseStage() : stage(0)
  {}

  std::size_t get_stage() const
  {
    return stage;
  }

  std::size_t next_stage()
  {
    return ++stage;
  }

  void reset()
  {
    stage = 0;
  }
};

template <typename InputDataType>
using ParseResult = std::pair<ParseState, InputDataType>;

template <typename InputDataType, typename ParserState, typename... Args>
using StageFunctionRefType = ParseResult<InputDataType> (&)(const InputDataType &, ParserState &, Args... args);

template <typename InputDataType, typename ParserState, typename... Args,
          StageFunctionRefType<InputDataType, ParserState, Args...>... StageFunctions>
class Pipeline
{
  static constexpr std::array Stages{ StageFunctions... };

public:
  static ParseResult<InputDataType> feed(const InputDataType &input_data, ParserState &parser_state,
                                         ParseStage &parse_stage, Args... args)
  {
    if (parser_state.parse_state == ParseState::REJECTED)
      return { ParseState::REJECTED, input_data };

    auto remaining_data = input_data;

    while (parse_stage.get_stage() < Stages.size() && remaining_data.size())
    {
      const auto parse_result = Stages[parse_stage.get_stage()](remaining_data, parser_state, args...);
      remaining_data = parse_result.second;
      if (parse_result.first == ParseState::EXTRACTED)
        parse_stage.next_stage();
      else if (parse_result.first == ParseState::REJECTED)
        return parse_result;
    }

    if (parse_stage.get_stage() == Stages.size())
      return { ParseState::EXTRACTED, remaining_data };

    return { ParseState::IN_PROGRESS, remaining_data };
  }
};

but I get an error message: parameter pack 'Args' must be at the end of the template parameter list

How can I fix this? Or do you have any other solutions? the performance is really important to me so I want to avoid using virtual or std::function.

1 Answers1

1

Based on How can I have multiple parameter packs in a variadic template? the key to allow what you aim for is by using template specialization.

You are looking for something like:

template <typename... Args>
using foo = void (*)(Args... args);

template <auto...>
class FoosHolder;

template <typename... Args,
          foo<Args...>... Foos>
class FoosHolder<Foos...>
{
public:
    static void call(Args... args) {
        return (..., Foos(args...));
    }
};

With the following usage example:

void foo1(int i) {
    std::cout << i << '\n';
}

void foo2(int i, double d){
    std::cout << "i = " << i << ", d = " << d << '\n';
}

void foo3(int i, double d){
    std::cout << "i * d = " << i * d << '\n';
}

int main() {
    FoosHolder<foo1> f1;
    FoosHolder<foo2, foo3> f2;
    f1.call(7);
    f2.call(2, 2.5);
}

Expected output would be:

7
i = 2, d = 2.5
i * d = 5

Above code, however, is a bit limiting. As it doesn't allow lambda as a function for FoosHolder. Which then raises the question of why do we need to declare foo to begin with. A probably better approach would be just to go with a simple template without a need for any specialization, something like that:

template <auto... Foos>
class FoosHolder
{
public:
    template <typename... Args>
    static void call(Args... args) {
        return (..., Foos(args...));
    }
};

And now we can also do (with C++20):

FoosHolder<[](){std::cout << "lambda!\n";}> f3;
f3.call();

FoosHolder<[](char c){std::cout << "lambda with params!" << c;}> f4;
f4.call('\n');

This approach has a minor drawback (a reasonable one IMHO), as it allows creation of a FoosHolder with functions that expect different amount of arguments, this will not be caught upon creation of the type but only when calling the call operation:

FoosHolder<foo1, foo3> f5; // passes compilation
f5.call(3, 1.5); // fails compilation only here
Amir Kirsh
  • 12,564
  • 41
  • 74
  • Thank you for your answer. It really helps me. I have a question when I change `template ` to `template` It doesn't compile. can you say please why? – Amir Hossein Sarebani Aug 24 '23 at 00:34
  • Because the functions sent to FoosHolder are not types, they are actual functions (i.e. non-type template parameters). So `typename` doesn't fit here and this is where `auto` comes to rescue as it can represent a non-type template parameter of any type :-) – Amir Kirsh Aug 24 '23 at 00:36
  • Great, my pleasure! – Amir Kirsh Aug 24 '23 at 00:44
  • @AmirKirsh: *"is a bit limiting. It doesn't allow lambda as a function for FoosHolder."* Converting non generic, non capturing lambda to function pointer is just an extra character (`+`/`*`). "Improved" code doesn't/cannot handle capture. So the only point is generic lambda. – Jarod42 Aug 24 '23 at 01:56