How can I work around this parameter expansion bug in GCC without upgrading to 8.x?

52 views Asked by At

Consider the following code:

#include <iostream>
#include <utility>
#include <array>
#include <functional>
#include <ctime>

template <unsigned N> void foo() { std::cout << N << "(" << ") "; }
template<> void foo<2>() { std::cout << "TWO (" <<  ") "; }

struct Foo {
    template <unsigned N> void operator()(std::integral_constant<unsigned,N>) { foo<N>(); }
};

template <std::size_t Offset, std::size_t... Idx, typename F>
void visit(F f, std::index_sequence<Idx...>, std::size_t n) {
    static std::array<std::function<void()>, sizeof...(Idx)> funcs {{
        [&f](){f(std::integral_constant<unsigned,Idx+Offset>{});}...
    }};

    funcs[n - Offset]();
};

template <std::size_t Start, std::size_t End, typename F>
void visit(F f, std::size_t n) {
    visit<Start>(f, std::make_index_sequence<End-Start>{}, n);
};

int main() {
    auto t = time(nullptr);
    for(int i = 0; i < 10; i++) {
        visit<1, 10>(Foo{}, (t+i) % 10);
    }
}

This is valid C++14 (actually, also valid C++11 if you write your own std::index_sequence). But - it doesn't compile with g++ 6.x and 7.x; only g++ 8.x compiles it properly (see this happening on GodBolt).

For organizational reasons, I can require use g++ version up to 7.2. Is there a way I could alter the code while keeping the semantics, to make g++ 7.x compile it?

2

There are 2 answers

0
T.C. On BEST ANSWER

GCC's bug is that it can't pack expand lambdas. So don't pack expand lambdas.

template<class F, std::size_t Idx>
void caller(F& f) { f(std:::integral_constant<unsigned, Idx>()); }

template <std::size_t Offset, std::size_t... Idx, typename F>
void visit(F f, std::index_sequence<Idx...>, std::size_t n) {
    using ptr_type = void (*)(F&);
    static constexpr ptr_type funcs[] = {&caller<F, Idx+Offset>...};
    funcs[n-Offset](f);
}
0
einpoklum On

I hit two birds with one stone and expanded this visitation mechanism to functions taking an arbitrary number of arguments. And whaddaya know - that meant moving the lambda into a helper function to avoid different parameter packs interfering with each other. As @T.C. suggests - that's what GCC is having trouble doing, so the problem is avoided.

template <std::size_t N, typename F, typename... Ts>
std::function<void(Ts...)> make_visitor(F f) {
    return 
        [&f](Ts... args) {
            f(std::integral_constant<std::size_t,N>{}, std::forward<Ts>(args)...);
        };
}

template <std::size_t Offset, std::size_t... Idx, typename F, typename... Ts>
void visit(F f, std::index_sequence<Idx...>, std::size_t n, Ts... args) {
    static std::array<std::function<void(Ts...)>, sizeof...(Idx)> funcs {{
        make_visitor<Idx+Offset, F, Ts...>(f)...
    }};
    funcs[n-Offset](std::forward<Ts>(args)...);
};

template <std::size_t Start, std::size_t End, typename F, typename... Ts>
void visit(F f, std::size_t n, Ts... args) {
    visit<Start>(f, std::make_index_sequence<End-Start>{}, n, std::forward<Ts>(args)...);
};