Search code examples
c++templatesvariadic-templates

C++ variadic template empty argument specialization


What's the correct way to write a specialization for an empty argument variadic template. Take bellow code as an example:

#include <iostream>
#include <memory>
#include <tuple>
#include <functional>
#include <cassert>

using namespace std;

struct message {
    int type;
};

struct X: message {
    int payload;
    X(): message{1} {
    }
};

struct Y: message {
    int payload;
    Y(): message{2} {
    }
};

struct Z: message {
    int payload;
    Z(): message{3} {
    }
};

template<typename T>
constexpr int message_type = -1;

template<>
constexpr int message_type<X> = 1;

template<>
constexpr int message_type<Y> = 2;

template<>
constexpr int message_type<Z> = 3;

struct M {
    int payload;
    M(int payload): payload{ payload } {
    }
};

template<typename P, typename T1, typename... Ts>
tuple<int, unique_ptr<M>> helper(unique_ptr<message> &msg, function<int(unique_ptr<T1>&)> fn1, function<int(unique_ptr<Ts>&)>... fn) {
    if (msg->type == message_type<T1>) {
        unique_ptr<T1> m(static_cast<T1*>(msg.release()));
        auto result = fn1(m);
        return {result, make_unique<M>(m->payload)};
    } else {
        return helper<void, Ts...>(msg, fn...);
    }
}

template<typename P>
tuple<int, unique_ptr<M>> helper(unique_ptr<message> &msg) {
    assert(false);
    return {0, unique_ptr<M>()};
}

template<typename... Ts>
tuple<int, unique_ptr<M>> dispatch_msg(unique_ptr<message> &msg, function<int(unique_ptr<Ts>&)> ...fn) {
    return helper<void, Ts...>(msg, fn...);
}

int main() {
    auto *real_message = new Z;
    real_message->payload = 101;

    unique_ptr<message> msg(real_message);

    auto [result, m] = dispatch_msg<X, Y, Z>(msg, [](auto &x) {
        return x->payload + 1;
    }, [](auto &y) {
        return y->payload + 2;
    }, [](auto &z) {
        return z->payload + 3;
    });
    cout << result << '\n' << m->payload << endl;
    return 0;
}

The helper function takes variadic template arguments. If it checked all given type arguments and failed. e.g. run to the empty arguments. I want to assert and stop the process. The current code works but I'm wondering is there any straightforward way to write a specialization.

I simplified the core requirements into the code below:

template<typename T, typename... Ts>
void func(int val, T arg, Ts... args) {
    if (condition_hold<T>(val)) {
        return;
    } else {
        return func<Ts...>(val, args...);
    }
}

template<>
void func(int val) {
    assert(false);
}

int main() {
    func<int, double, float>(100);
    return 0;
}

Basically the func is checking against every given type whether a condition hold for the input val. If all check failed I want to do something, like the assert here. So I wrote a specialization takes empty argument, but this can't compile.


Solution

  • In C++17, you don't need to split parameter packs into head and tail in most cases. Thanks to fold expressions, many operations on packs become much easier.

    // Some generic predicate.
    template <typename T>
    bool condition_hold(T) {
        return true;
    }
    
    // Make this whatever you want.
    void do_something_with(int);
    
    template<typename... Ts>
    auto func(int val, Ts... args) {
        // Fold expression checks whether the condition is true for all
        // elements of the parameter pack.
        // Will be true if the parameter pack is empty.
        if ((condition_hold(args) && ...))
            do_something_with(val);
    }
    
    int main() {
        // Ts type parameters are deduced to <float, float>.
        func(100, 1.f, 2.f);
        return 0;
    }
    

    To check whether the pack was empty and handle this case specially, you can do:

    template<typename... Ts>
    auto func(int val, Ts... args) {
        if constexpr (sizeof...(Ts) == 0) {
            // handle empty pack
        }
        else {
            // handle non-empty pack
        }
    }
    

    Your specialization couldn't have worked because func<> needs to take at least one parameter. A specialization such as

    template<typename T>
    void func<T>(int val);
    

    Wouldn't be valid either, because it wold be a partial specialization which is only allowed for classes. However, if the base template only takes a pack, we can fully specialize it:

    template<typename... Ts>
    void func(int val, Ts... args);
    
    template<>
    void func<>(int val);