Como posso criar uma maneira cartesiana de produtos de listas de tipos em C ++?

26

Auto-explicativo.

Basicamente, digamos que eu tenha listas de tipos assim:

using type_list_1 = type_list<int, somestructA>;
using type_list_2 = type_list<somestructB>;
using type_list_3 = type_list<double, short>;

Eles podem ser um número variável de listas de tipos.

Como obtenho uma lista de tipos de produto cartesiano?

result = type_list<
type_list<int, somestructB, double>,
type_list<int, somestructB, short>,
type_list<somestructA, somestructB, double>,
type_list<somestructA, somestructB, short>
>;

Eu falei sobre como criar um produto cartesiano bidirecional, conforme indicado aqui: Como criar o produto cartesiano de uma lista de tipos? , mas n não parece ser tão trivial.

Por enquanto estou tentando ...

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

// To concatenate
template <typename... Ts, typename... Us>
constexpr auto operator|(type_list<Ts...>, type_list<Us...>) {
   return type_list{Ts{}..., Us{}...};
}

template <typename T, typename... Ts, typename... Us>
constexpr auto cross_product_two(type_list<T, Ts...>, type_list<Us...>) {
    return (type_list<type_list<T,Us>...>{} | ... | type_list<type_list<Ts, Us>...>{});
}

template <typename T, typename U, typename... Ts>
constexpr auto cross_product_impl() {
    if constexpr(sizeof...(Ts) >0) {
        return cross_product_impl<decltype(cross_product_two(T{}, U{})), Ts...>();
    } else {
        return cross_product_two(T{}, U{});
    }
}

Vou apenas dizer que, considerando o quão difícil é acertar, use o boost como na resposta de Barry. Infelizmente eu tenho que ficar preso a uma abordagem rolada à mão, porque usar ou não o boost é uma decisão que vem de outro lugar :(

themagicalyang
fonte
8
Oof, você é um glutão por punição 😏
Lightness Races in Orbit
Eu meio que sou péssima, mas você pode modificar o produto cartesiano bidirecional de uma maneira que: 1) a primeira lista de tipos seja na verdade uma lista de tipos de um tipo; 2) em vez de concatenar dois tipos de listas de tipos, a metafunção acrescentaria os tipos da segunda lista às listas "filho" da primeira lista de tipos (de maneira cartesiana do produto)? Se possível, o problema pode ser facilmente resolvido com o algoritmo recursivo.
smitsyn
11
A dificuldade real em uma implementação recursiva é que cartesian_producté uma lista de listas de tipos e, a cada etapa da recursão, você deseja anexar itens a cada lista de tipos interna. Entrar nesse segundo nível de embalagem de embalagem leva algum dedução ...
Max Langhof
11
Eu acho que você também pode implementá-lo "linearmente" olhando para isso como um "espaço de texto" N-dimensional, onde você deseja atravessar cada "ponto de grade de texto". Você calcula o número de pontos da grade, depois apenas o percorre como faria através de uma matriz ND achatada e calcula os tipos em cada ponto da grade. Algo a considerar ...
Max Langhof
11
@MaxLanghof Algo parecido com " Um produto cartesiano de tuplas em C ++ 17 "?
Deduplicator

Respostas:

14

Com o Boost.Mp11 , este é um one-liner curto (como sempre):

using result = mp_product<
    type_list,
    type_list_1, type_list_2, type_list_3>;

Demo .

Barry
fonte
11
Vaca sagrada ... Mas me sinto obrigado a apontar que (amostrando cada código várias vezes no godbolt) a versão Mp11 leva cerca de duas vezes mais tempo para compilar. Não tenho certeza quanto dessa sobrecarga está analisando o cabeçalho de impulso propriamente dito e quanto está instanciando modelos ...
Max Langhof 18 /
11
@MaxLanghof Sure. 1.5x se você incluir apenas em algorithm.hppvez de todo o Mp11. E mesmo assim estamos falando de 0,08s contra 0,12s. Tenho que levar em consideração quanto tempo levei para escrever isso também.
Barry
8
@ Barry: Do ponto de vista de engenharia de software, com você 100%. Também é fácil ler isso em comparação com uma abordagem manual. Além disso, há pouco ou nenhum teste necessário para garantir a correção da solução da biblioteca. Em geral, menos código e maior confiança levarão a menores custos de manutenção por toda a vida útil.
AndyG 19/12/19
Concordo que isso é bastante simples, mas infelizmente existem equipes que desaprovam o impulso.
themagicalyang
há equipes que desaprovam tudo. este não é um motivo para não usá-lo.
Tomaz Canabrava
13

OK, entendi. Não é bonito, mas funciona:

template<class ... T>
struct type_list{};

struct somestructA{};
struct somestructB{};

using type_list_1 = type_list<int, somestructA, char>;
using type_list_2 = type_list<somestructB>;
using type_list_3 = type_list<double, short, float>;

template<class TL1, class TL2>
struct add;

template<class ... T1s, class ... T2s>
struct add<type_list<T1s...>, type_list<T2s...>>
{
    using type = type_list<T1s..., T2s...>;
};

template<class ... TL>
struct concat;

template<class TL, class ... TLs>
struct concat<TL, TLs...>
{
    using type = typename add<TL, typename concat<TLs...>::type>::type;
};

template<class TL>
struct concat<TL>
{
    using type = TL;
};

static_assert(std::is_same_v<type_list<int, somestructA, char, double, short, float>, typename add<type_list_1, type_list_3>::type>);

template<class TL1, class TL2>
struct multiply_one;

// Prepends each element of T1 to the list T2.
template<class ... T1s, class ... T2s>
struct multiply_one<type_list<T1s...>, type_list<T2s...>>
{
    using type = typename concat<type_list<type_list<T1s, T2s...>...>>::type;
};

static_assert(std::is_same_v<
    type_list<
        type_list<int, double, short, float>,
        type_list<somestructA, double, short, float>,
        type_list<char, double, short, float>
        >,
    typename multiply_one<type_list_1, type_list_3>::type>);

// Prepends each element of TL to all type lists in TLL.
template<class TL, class TLL>
struct multiply_all;

template<class TL, class ... TLs>
struct multiply_all<TL, type_list<TLs...>>
{
    using type = typename concat<typename multiply_one<TL, TLs>::type...>::type;
};

static_assert(std::is_same_v<
    type_list<
        type_list<int, double, short, float>,
        type_list<somestructA, double, short, float>,
        type_list<char, double, short, float>
        >,
    typename multiply_all<type_list_1, type_list<type_list_3>>::type>);

static_assert(std::is_same_v<
    type_list<
        type_list<int, somestructB>,
        type_list<somestructA, somestructB>,
        type_list<char, somestructB>,
        type_list<int, double, short, float>,
        type_list<somestructA, double, short, float>,
        type_list<char, double, short, float>
        >,
    typename multiply_all<type_list_1, type_list<type_list_2, type_list_3>>::type>);

template<class TL, class ... TLs>
struct cartesian_product
{
    using type = typename multiply_all<TL, typename cartesian_product<TLs...>::type>::type;
};

template<class ... Ts>
struct cartesian_product<type_list<Ts...>>
{
    using type = type_list<type_list<Ts>...>;
};


using expected_result = type_list<
    type_list<int, somestructB, double>,
    type_list<somestructA, somestructB, double>,
    type_list<char, somestructB, double>,
    type_list<int, somestructB, short>,
    type_list<somestructA, somestructB, short>,
    type_list<char, somestructB, short>,
    type_list<int, somestructB, float>,
    type_list<somestructA, somestructB, float>,
    type_list<char, somestructB, float>
>;

static_assert(std::is_same_v<expected_result,
    cartesian_product<type_list_1, type_list_2, type_list_3>::type>);

https://godbolt.org/z/L5eamT

Deixei meus próprios static_asserttestes lá por ... Bem, espero que eles ajudem.

Além disso, tenho certeza de que deve haver uma solução melhor. Mas esse era o caminho óbvio "Eu sei que isso acabará por levar ao objetivo". Eventualmente, tive que recorrer a adicionar concattipos, tenho certeza de que poderia ser usado muito mais cedo para pular a maior parte do lixo.

Max Langhof
fonte
4
Programação de modelos que eu posso seguir. Fantástico. Eu aprendi algo hoje.
Jerry Jeremiah
add usa duas listas de tipos. Como você está passando várias listas de tipos para adicionar no concat?
themagicalyang
@themagicalyang Bem visto, isso é um bug (que os testes não encontraram porque todas as listas envolvidas tinham apenas o tamanho 2). Ele ...precisa entrar na concatchamada recursiva , não fora. Resposta (incluindo casos de teste) corrigida. Prova Barry direito em relação às expectativas de correção :)
Max Langhof
A chamada do produto cartesiano não é multiplicar_totalmente um número_ múltiplo?
themagicalyang
@themagicalyang No. cartesian_productimplementa a recursão. multiply_allfaz uma multiply_onepara cada lista de tipos no TLspacote. cartesian_product::typeé uma lista de listas de tipos. multiply_allpega uma lista de tipos e uma lista de listas de tipos. multiply_oneleva duas listas do tipo a1, a2, a3e b1, b2, b3e cria a1, b1, b2, b3, a2, b1, b2, b3, a3, b1, b2, b3. Você precisa desses dois níveis de dedução ( multiply_all, multiply_one) porque precisa descer dois níveis de "variabilidade"; veja meu primeiro comentário sobre a questão.
precisa
9

Dobre expressões para o resgate novamente

template<typename... Ts>
typelist<typelist<Ts>...> layered(typelist<Ts...>);

template<typename... Ts, typename... Us>
auto operator+(typelist<Ts...>, typelist<Us...>)
    -> typelist<Ts..., Us...>;

template<typename T, typename... Us>
auto operator*(typelist<T>, typelist<Us...>)
    -> typelist<decltype(T{} + Us{})...>;

template<typename... Ts, typename TL>
auto operator^(typelist<Ts...>, TL tl)
    -> decltype(((typelist<Ts>{} * tl) + ...));

template<typename... TLs>
using product_t = decltype((layered(TLs{}) ^ ...));

E você terminou. Isso tem o benefício adicional sobre a recursão de ter profundidade de instanciação O (1).

struct A0;
struct A1;
struct B0;
struct B1;
struct C0;
struct C1;
struct C2;

using t1 = typelist<A0, A1>;
using t2 = typelist<B0, B1>;
using t3 = typelist<C0, C1, C2>; 

using p1 = product_t<t1, t2>;
using p2 = product_t<t1, t2, t3>;

using expect1 = typelist<typelist<A0, B0>,
                         typelist<A0, B1>,
                         typelist<A1, B0>,
                         typelist<A1, B1>>;

using expect2 = typelist<typelist<A0, B0, C0>,
                         typelist<A0, B0, C1>,
                         typelist<A0, B0, C2>,
                         typelist<A0, B1, C0>,
                         typelist<A0, B1, C1>,
                         typelist<A0, B1, C2>,
                         typelist<A1, B0, C0>,
                         typelist<A1, B0, C1>,
                         typelist<A1, B0, C2>,
                         typelist<A1, B1, C0>,
                         typelist<A1, B1, C1>,
                         typelist<A1, B1, C2>>;

static_assert(std::is_same_v<p1, expect1>);
static_assert(std::is_same_v<p2, expect2>);
Passer By
fonte
Isso me intriga. Existe uma maneira de representá-lo como resultado TL1 * TL2 * TL3 = crossporduct?
themagicalyang
@themagicalyang O que você quer dizer com "resultado entre produtos"?
Passer Por
basicamente em vez de using result = product_t<t1,t2,t3>... alguma maneira de representá-lo como using result = decltype(t1{} * t2{} * t3{});. Hmm, bem, agora que ele pensa sobre isso, uma vez que decltypeé inevitável, simplesmente usar o alias que você deu é mais intuitivo.
themagicalyang
Interessante! O uso de sobrecarga de operador fornece expressões de dobra em vez das recursões que eu tinha que fazer. Também o torna muito mais conciso. Vou manter isso em mente para a próxima vez!
precisa
@PasserBy Todos esses operadores e funções auxiliares precisam estar no mesmo espaço para nome? Estou tendo problemas ao colocar tudo dentro de um espaço para nome e acessar o product_t usando um alias do espaço para nome externo.
themagicalyang