Home > Blockchain >  C unpack variadic template parameters and call another template method
C unpack variadic template parameters and call another template method

Time:12-29

I am trying to unpack variadic template parameters, and call "read<>" on each of them. And then collect the results of "read<>" to create a new tuple object.

This is what I wrote after learning "fold expression" in C 17:

    template<typename ...U>
    std::tuple<U...> read_impl(std::tuple<U...>* arg) {
        // The argument exists for function overloading. As an alternative of partial specialization.
        using T = std::tuple<U...>;
        return T(read<U>(), ...);
    }

And I got Fatal Error C1001 INTERNAL COMPILER ERROR.

Intended usage:

std::tuple<int, float, double> tup = read_impl(static_cast<std::tuple<int, float, double>*>(0));
// which is equivalent to
std::tuple<int, float, double> tup = std::make_tuple(read<int>(), read<float>(), read<double>());

And this is what I am currently doing, which is ugly and verbose without unpacking magic.

    template<typename U1>
    std::tuple<U1> read_impl(std::tuple<U1>* arg) {
        using T = std::remove_pointer<decltype(arg)>::type;
        return T(read<U1>());
    }
    template<typename U1, typename U2>
    std::tuple<U1, U2> read_impl(std::tuple<U1, U2>* arg) {
        using T = std::remove_pointer<decltype(arg)>::type;
        return T(read<U1>(), read<U2>());
    }
    template<typename U1, typename U2, typename U3>
    std::tuple<U1, U2, U3> read_impl(std::tuple<U1, U2, U3>* arg) {
        using T = std::remove_pointer<decltype(arg)>::type;
        return T(read<U1>(), read<U2>(), read<U3>());
    }
    ...

Here is a minimal reproducible case:

#include <tuple>

class Reader {
public:
    template<typename T>
    T read() {
        return read_impl(static_cast<T*>(0));
    }
    int read_impl(int*) {
        return 1;
    }
    float read_impl(float*) {
        return 0.5f;
    }
    template<typename ...U>
    std::tuple<U...> read_impl(std::tuple<U...>* arg) {
        using T = std::tuple<U...>;
        return T(read<U>(), ...);
    }

    static void test() {
        Reader reader;
        auto result = reader.read<std::tuple<int, float>>();
        assert(std::get<0>(result) == 1);
        assert(std::get<1>(result) == 0.5f);
    }
};

int main() {
    Reader::test();
    return 0;
}

=== Reply to some comments/answers: ===

@kiner_shah Why a raw pointer to a tuple? And why aren't you using arg in your read_impl?

Because if I want to specialize the generic "f" function based on different return value, it will not compile.

#include <exception>
#include <assert.h>

class Reader {
public:
    template<typename T>
    T f() {
        throw std::exception();
    }

    template<>
    int f() {
        return 1;
    }

    template<>
    float f() {
        return 0.5f;
    }

    static void test_f() {
        Reader reader;
        auto result = reader.f<int>();
        assert(result == 1);
    }
};

int main() {
    Reader::test_f();
    return 0;
}

=======

@Ted Lyngmo Your minimal reproducible example does not compile using any compiler.

That's why I am asking this question. I want it to compile.

Here is a more complete example of Reader. It needs to be able to read complex types like this: reader.read<std::unordered_map<int, std::vector<std::string>>>().

#include "pch.h"
#include <unordered_map>
#include <unordered_set>
#include <tuple>
#include <string>
#include <optional>

class Reader {
public:
    template<typename T>
    T read() {
        return read_impl(static_cast<T*>(0));
    }

    template<typename T>
    T read_impl(T*) {
        T::deserialize(*this);
    }
    uint64_t read_impl(uint64_t*) {
        return 1; // this will not be a constant in the real situation.
    }
    uint32_t read_impl(uint32_t*) {
        return 1; // this will not be a constant in the real situation.
    }
    int read_impl(int*) {
        return 1; // this will not be a constant in the real situation.
    }
    float read_impl(float*) {
        return .5f; // this will not be a constant in the real situation.
    }
    bool read_impl(bool*) {
        return true; // this will not be a constant in the real situation.
    }
    std::string read_impl(std::string*) {
        return "readString"; // this will not be a constant in the real situation.
    }
    template<typename T>
    std::vector<T> read_impl(std::vector<T>*) {
        size_t size = read<uint64_t>();
        std::vector<T> r;
        r.reserve(size);
        for (size_t i = 0; i < size; i  ) {
            r.push_back(read<T>());
        }
        return r;
    }
    template<typename TKey, typename TValue>
    std::unordered_map<TKey, TValue> read_impl(std::unordered_map<TKey, TValue>*) {
        size_t size = read<uint64_t>();
        std::unordered_map<TKey, TValue> r;
        r.reserve(size);
        for (size_t i = 0; i < size; i  ) {
            TKey k = read<TKey>();
            TValue v = read<TValue>();
            r[k] = v;
        }
        return r;
    }
    template<typename T>
    std::unordered_set<T> read_impl(std::unordered_set<T>*) {
        size_t size = read<uint64_t>();
        std::unordered_set<T> r;
        r.reserve(size);
        for (size_t i = 0; i < size; i  ) {
            r.insert(read<T>());
        }
        return r;
    }
    template<typename ...U>
    std::tuple<U...> read_impl(std::tuple<U...>* arg) {
        using T = std::tuple<U...>;
        return T{ read<U>() ... };
    }
    template<typename T>
    std::optional<T> read_impl(std::optional<T>* t) {
        bool hasValue = read<bool>();
        std::optional<T> r;
        if (hasValue) {
            r = read<T>();
        }
        return r;
    }

    static void test() {
        Reader reader;
        auto result = reader.read<std::tuple<int, float>>();
        assert(std::get<0>(result) == 1);
        assert(std::get<1>(result) == 0.5f);
        auto result2 = reader.read<std::unordered_map<int, std::vector<std::string>>>();
        assert(result2[1] == std::vector<std::string>{"readString"});
    }
};

int main() {
    Reader::test();
    return 0;
}

=======

@HolyBlackCat and @Jarod42 Thanks so much for pointing out the evaluation order issue in the code.

CodePudding user response:

I would drop the specializations and use if constexpr. You also do not need pointers to tuples.

Example:

class Reader {
public:
    template <typename T>
    constexpr std::tuple<T> read() {
        // return different values depending on type:
        if constexpr (std::is_same_v<T, int>) return 1;
        if constexpr (std::is_same_v<T, float>) return 0.5f;
        return {};
    }

    // an overload that requires at least 2 template parameters:
    template <typename T, typename U, typename... V>
    constexpr std::tuple<T, U, V...> read() {
        return std::tuple_cat(read<T>(), read<U>(), read<V>()...);
    }

    static void test() {
        Reader reader;
        constexpr auto result = reader.read<int, float>();
        static_assert(std::is_same_v<decltype(result),
                                     const std::tuple<int, float>>);
        static_assert(std::get<0>(result) == 1);
        static_assert(std::get<1>(result) == 0.5f);
    }
};

Demo

If you really want to keep the implementations for reading int and float separate instead of using if constexpr, you can:

    template <typename T>
    requires std::is_same_v<T, int>
    constexpr std::tuple<T> read() {
        return 1;
    }
    
    template <typename T>
    requires std::is_same_v<T, float>
    constexpr std::tuple<T> read() {
        return 0.5f;
    }

If you need to be able to read not only into tuples, you could make it even more generic without the need for pointers to the types you want to read from.

// type trait to check if a type is a tuple:
template <typename...> struct is_tuple : std::false_type {};
template <typename... T> struct is_tuple<std::tuple<T...>> : std::true_type {};
template<class T>
inline constexpr bool is_tuple_v = is_tuple<T>::value;

class Reader {
public:
    template <typename T>
    requires std::is_same_v<T, int>
    constexpr int read() { return 1; }

    template <typename T>
    requires std::is_same_v<T, float>
    constexpr float read() { return 0.5f; }

    // and it supports more complex types, like tuples:
    template <typename T>
    requires is_tuple_v<T>
    constexpr T read() {
        return [this]<std::size_t... I>(std::index_sequence<I...>) -> T {
            return {this->read<std::tuple_element_t<I, T>>()...};
        }(std::make_index_sequence<std::tuple_size_v<T>>{});
    }

    static void test() {/* same as above */ }
};

Demo

CodePudding user response:

Based on comments made by @HolyBlackCat and @Jarod42, T(read<U>(), ...) should be T{ read<U>() ... }.

    template<typename ...U>
    std::tuple<U...> read_impl(std::tuple<U...>* arg) {
        using T = std::tuple<U...>;
        return T{ read<U>() ... };
    }

I was reading fold expression feature (https://en.cppreference.com/w/cpp/language/fold) and now I've learnt that "fold/reduce" is not same as "expanding".

  • Related