Home > database >  Forward variadic arguments based on conditions
Forward variadic arguments based on conditions

Time:06-24

I would like to achieve following behavior:

enum class Fruit {
 Apple,
 Orange,
 Pear,
};

template<typename... Args>
void processFruit(Fruit fruit_type, Args&&... args) {
 switch (fruit_type) {
   case Apple:
     processApple(std::forward<Args>args...);
     break;
   case Orage:
     processOrange(std::forward<Args>(args)...);
     break;
   default:
     break;
 }
 postProcessing();
 return;
}

processApple and processOrange take different number of arguments. This of course cannot compile. Compiler cannot instantiate functions based on the switch statement. I found some suggestions on static_if. Just wondering if there's a simpler way for my situation. Thanks.

p.s. my platform restricts me on C 14 solutions.

CodePudding user response:

Your question muddies the waters a bit, because you're mixing runtime and compile-time logic, because in your case the number and types of function arguments depends on the runtime value of something.

You're now stuck with two different options: it is possible to make this work (even in just C 14), but any call site that doesn't pass the correct arguments will not be detected until runtime. The way to do so is with the help of std::enable_if_t, decltype() and std::declval. I've done this for your example, and the following compiles for me in C 14 mode:

#include <iostream>
#include <type_traits>
#include <stdexcept>

enum class Fruit {
Apple,
Orange,
Pear,
};

void processApple(int a, int b);
void processOrange(int c);
void processPear(double d);
void postProcessing();

struct Processor
{
    template<typename... Ts, typename = decltype(processApple(std::forward<Ts>(std::declval<Ts>())...))>
    static void process(std::integral_constant<Fruit, Fruit::Apple>, Ts&&... ts)
    {
        processApple(std::forward<Ts>(ts)...);
    }

    template<typename... Ts, typename = decltype(processOrange(std::forward<Ts>(std::declval<Ts>())...))>
    static void process(std::integral_constant<Fruit, Fruit::Orange>, Ts&&... ts)
    {
        processOrange(std::forward<Ts>(ts)...);
    }

    template<typename... Ts, typename = decltype(processPear(std::forward<Ts>(std::declval<Ts>())...))>
    static void process(std::integral_constant<Fruit, Fruit::Pear>, Ts&&... ts)
    {
        processPear(std::forward<Ts>(ts)...);
    }

    [[noreturn]] static void process(...)
    {
        // While this is inserted, it should never be called in practice,
        // unless there's a programming error.
        throw std::invalid_argument("Invalid argument specified to processFruit().");
    }
};

template<typename... Args>
void processFruit(Fruit fruit_type, Args&&... args) {
    switch (fruit_type) {
    case Fruit::Apple:
        Processor::process(std::integral_constant<Fruit, Fruit::Apple>(), std::forward<Args>(args)...);
        break;
    case Fruit::Orange:
        Processor::process(std::integral_constant<Fruit, Fruit::Orange>(), std::forward<Args>(args)...);
        break;
    case Fruit::Pear:
        Processor::process(std::integral_constant<Fruit, Fruit::Pear>(), std::forward<Args>(args)...);
        break;
    default:
        break;
    }
    postProcessing();
    return;
}

void processApple(int a, int b)
{
    std::cout << "Apple: " << a << " // " << b << std::endl;
}

void processOrange(int c)
{
    std::cout << "Orange: " << c << std::endl;
}

void processPear(double d)
{
    std::cout << "Pear: " << d << std::endl;
}

void postProcessing()
{
    std::cout << "Post processing" << std::endl;
}

int main()
{
    processFruit(Fruit::Apple, 4, 8);
    processFruit(Fruit::Orange, 15);
    processFruit(Fruit::Pear, 1.234);
    // The following will only throw an exception, but not fail to compile
    //processFruit(Fruit::Pear, 1.234, 77);
    return 0;
}

(The struct Processor is just to clean up the namespace, they could be global functions.) Problem here is that the compiler can't detect a wrong call, see the commented out call to processFruit(Fruit::Pear, 1.234, 77); that would generate an exception, but the compiler couldn't detect it at compile time.

In my eyes this is not very sensible though. Since the arguments depend on the Fruit type anyway, I don't really see how you could even perform a call to one of these functions when the Fruit argument is only known at runtime, because each call site will only work for a single type.

And if you do know that at compile time anyway, this could be done in a much simpler way via overloads, that also allows for much better compile-time diagnostics:

#include <iostream>
#include <type_traits>

enum class Fruit {
Apple,
Orange,
Pear,
};

void processApple(int a, int b);
void processOrange(int c);
void processPear(double d);
void postProcessing();

template<typename... Args>
void processFruit2Helper(std::integral_constant<Fruit, Fruit::Apple>, Args&&... args)
{
    processApple(std::forward<Args>(args)...);
    // or put the code of processApple directly in here
}

template<typename... Args>
void processFruit2Helper(std::integral_constant<Fruit, Fruit::Orange>, Args&&... args)
{
    processOrange(std::forward<Args>(args)...);
    // or put the code of processOrange directly in here
}

template<typename... Args>
void processFruit2Helper(std::integral_constant<Fruit, Fruit::Pear>, Args&&... args)
{
    processPear(std::forward<Args>(args)...);
    // or put the code of processPear directly in here
}

template<Fruit f, typename... Args>
void processFruit2(Args&&... args)
{
    processFruit2Helper(std::integral_constant<Fruit, f>(), std::forward<Args>(args)...);
    postProcessing();
}

void processApple(int a, int b)
{
    std::cout << "Apple: " << a << " // " << b << std::endl;
}

void processOrange(int c)
{
    std::cout << "Orange: " << c << std::endl;
}

void processPear(double d)
{
    std::cout << "Pear: " << d << std::endl;
}

void postProcessing()
{
    std::cout << "Post processing" << std::endl;
}

int main()
{
    processFruit2<Fruit::Apple>(4, 8);
    processFruit2<Fruit::Orange>(15);
    processFruit2<Fruit::Pear>(1.234);

    // The following will fail to compile here (which is good)
    //processFruit2<Fruit::Pear>(1.234, 77);
    return 0;
}

That all said, I suspect there's some higher-level design problem in your code, but we won't be able to understand that with the limited example you've provided.

  • Related