Создать constexpr array/vector из вызовов шаблонной функции

Рейтинг: 1Ответов: 2Опубликовано: 28.07.2023

Нужна довольно странная вещь. Имеются вызовы некой шаблонной функции

foo<1>();
foo<2>();
foo<5>();

Имея только эти вызовы, нужно каким-то образом получить вектор/массив/дерево/что угодно, что будет хранить

{1, 2, 5}

Подвох в том, что вектор/дерево/массив, хранящий в себе {1, 2, 5} должен быть создан до вызовов шаблонной функции. Иными словами, имея только саму декларацию этих вызовов(они могут быть не вызваны вовсе), нужно получить контейнер.

Это возможно? И если возможно, как?

UPD: в целом, можно и не контейнер получить, а например другую функцию вида

bar<1, 2, 5>();

Ответы

▲ 3

Этот трюк существует в двух вариантах:

  1. Массив заполняется сразу при запуске программы. Туда попадают элементы из всех единиц трансляции. Такой список, очевидно, не может constexpr, потому что каждая единица трансляции компилируется независимо.

  2. Массив заполняется во время компиляции. Тут он спокойно может быть constexpr, но видит элементы только из текущей единицы трансляции, и все элементы должны быть зарегистрированы выше по тексту.

Вариант (1) в целом уже написан в другом ответе, но приведу немного другое исполнение, которое мне больше нравится - без замены функции на шаблонную переменную с operator() и т.п.:

#include <iostream>
#include <set>
#include <type_traits>

namespace impl
{
    std::set<int> &MutRegistry()
    {
        // Переменная обязательно внутри функции, чтобы избежать static init order fiasco.
        static std::set<int> ret;
        return ret;
    }

    template <int N>
    static const auto register_value = []{
        MutRegistry().insert(N);
        return nullptr;
    }();
}

const std::set<int> &Registry()
{
    return impl::MutRegistry();
}

template <int N>
void foo()
{
    (void)std::integral_constant<const std::nullptr_t *, &impl::register_value<N>>{};
}

int main()
{
    for (int x : Registry())
        std::cout << x << '\n'; // 10, 20

    foo<10>();
    foo<20>();
}

Вариант (2) пишется сильно сложнее, нужно использовать stateful metaprogramming.

#include <array>
#include <cstddef>
#include <iostream>
#include <type_traits>
#include <utility>

template <typename T>
struct tag {using type = T;};

template <auto V>
struct value_tag {static constexpr auto value = V;};

template <typename...>
struct type_list {};

namespace StatefulList
{
    namespace // Hopefully guard against ODR violations?
    {
        namespace impl
        {
            template <typename Name, std::size_t Index>
            struct ElemReader
            {
                #if defined(__GNUC__) && !defined(__clang__)
                #pragma GCC diagnostic push
                #pragma GCC diagnostic ignored "-Wnon-template-friend"
                #endif
                friend constexpr auto adl_ListElem(ElemReader<Name, Index>);
                #if defined(__GNUC__) && !defined(__clang__)
                #pragma GCC diagnostic pop
                #endif
            };

            template <typename Name, std::size_t Index, typename Value>
            struct ElemWriter
            {
                friend constexpr auto adl_ListElem(ElemReader<Name, Index>)
                {
                    return tag<Value>{};
                }
            };

            [[maybe_unused]] constexpr void adl_ListElem() {} // A dummy ADL target.

            template <typename Name, std::size_t Index, typename Unique, typename = void>
            struct CalcSize : std::integral_constant<std::size_t, Index> {};

            template <typename Name, std::size_t Index, typename Unique>
            struct CalcSize<Name, Index, Unique, decltype(void(adl_ListElem(ElemReader<Name, Index>{})))> : CalcSize<Name, Index + 1, Unique> {};

            template <typename Name, std::size_t Index, typename Unique>
            using ReadElem = typename decltype(adl_ListElem(ElemReader<Name, Index>{}))::type;

            template <typename Name, typename I, typename Unique>
            struct ReadElemList {};
            template <typename Name, std::size_t ...I, typename Unique>
            struct ReadElemList<Name, std::index_sequence<I...>, Unique> {using type = type_list<ReadElem<Name, I, Unique>...>;};
        }

        struct DefaultUnique {};

        template <typename T>
        struct DefaultPushBackUnique {};

        // Calculates the current list size.
        template <typename Name, typename Unique = DefaultUnique>
        [[maybe_unused]] inline constexpr std::size_t size = impl::CalcSize<Name, 0, Unique>::value;

        // Touch this type to append `Value` to the list.
        template <typename Name, typename Value, typename Unique = DefaultPushBackUnique<Value>>
        using PushBack = impl::ElemWriter<Name, size<Name, Unique>, Value>;

        // Returns the type previously passed to `WriteState`, or causes a SFINAE error.
        template <typename Name, std::size_t I, typename Unique = DefaultUnique>
        using Elem = impl::ReadElem<Name, I, Unique>;

        // Returns the list elements as `Meta::TypeList<...>`.
        template <typename Name, typename Unique = DefaultUnique>
        using Elems = typename impl::ReadElemList<Name, std::make_index_sequence<size<Name, Unique>>, Unique>::type;
    }
}

struct FooList {};

namespace // Guard against ODR violations?
{
    template <typename Unique = void>
    consteval auto FooElems()
    {
        return []<typename ...P>(type_list<P...>){
            return std::array<int, StatefulList::size<FooList, Unique>>{
                P::value...
            };
        }(StatefulList::Elems<FooList, Unique>{});
    }
}

template <int N>
auto foo() -> decltype(void(StatefulList::PushBack<FooList, value_tag<N>>{}))
{
}

int main()
{
    if (false) // Не мешает.
    {
        foo<10>();
        foo<20>();
    }
    for (int x : FooElems())
        std::cout << x << '\n'; // 10, 20
    foo<30>();
    for (int x : FooElems())
        std::cout << x << '\n'; // 10, 20 - wrong! Cached from the previous call.
    struct UniqueType {};
    for (int x : FooElems<UniqueType>())
        std::cout << x << '\n'; // 10, 20, 30 - correct
}
▲ 2

Можно добиться такого эффекта с помощью статических полей шаблонного класса.

#include <iostream>
#include <vector>

struct Registrator {
public:
    static const std::vector<int>& registered() {
        return reg();
    }
private:
    Registrator(int N) {
        reg().push_back(N);
    }
    static std::vector<int>& reg() {
        static std::vector<int> values;
        return values;
    }
    template<int N>
    friend class MyFunc;
};

template <int N>
struct MyFunc {
public:
    MyFunc() {
        (void)reg_;
    }
    void operator()() const {
        //Тело функции
        //...
        std::cout << N << '\n';
    };
private:
    static Registrator reg_;
};

template<int N>
Registrator MyFunc<N>::reg_ = {N};

template <int N>
MyFunc<N> func;

int main(int argc, char *argv[])
{
    for (int n : Registrator::registered()) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
    func<1>();
    func<2>();
    func<5>();
}