É possível corresponder parâmetros de modelo inteiro recursivamente em C ++?


8

Eu tenho o seguinte problema. Eu defino um vetor dimensional N como

#include <vector>
#include <utility>
#include <string>


template <int N, typename T>
struct NVector{
    typedef std::vector<typename NVector<N-1,T>::type> type;
};
template <typename T> struct NVector<1,T> {
    typedef std::vector<T> type;
};

Desejo escrever um mapa de função de ordem superior que possa transformar os elementos da folha do vetor aninhado, independentemente da profundidade e retornar um novo vetor aninhado da mesma forma. eu tentei


template <int N, typename T, typename Mapper>
struct MapResult {
    typedef decltype ( (std::declval<Mapper>()) (std::declval<T>()) ) basic_type;
    typedef typename NVector<N, basic_type>::type vector_type;
};

template <int N, typename T, typename Mapper>
typename MapResult<N,T,Mapper>::vector_type 
    Map( typename NVector<N,T>::type const & vector,  Mapper mapper)
{
    typename MapResult<N,T,Mapper>::vector_type out;
    for(auto i = vector.begin(); i != vector.end(); i++){
        out.push_back(Map(*i,mapper));
    }
    return out;
}

template <typename T, typename Mapper>
typename MapResult<1,T,Mapper>::vector_type  
    Map(typename NVector<1,T>::type const & vector, Mapper mapper)
{
    typename MapResult<1,T,Mapper>::vector_type out;
    for(auto i = vector.begin(); i != vector.end(); i++){
        out.push_back(mapper(*i));
    }
    return out;
}

e depois usá-lo como principal

int main(){

    NVector<1,int>::type a = {1,2,3,4};
    NVector<2,int>::type b = {{1,2},{3,4}};

    NVector<1,std::string>::type as = Map(a,[](int x){return std::to_string(x);});
    NVector<2,std::string>::type bs = Map(b,[](int x){return std::to_string(x);});
}

No entanto, recebo erros de compilação

<source>:48:34: error: no matching function for call to 'Map'

    NVector<1,double>::type da = Map(a,[](int x)->int{return (double)x;});

                                 ^~~

<source>:20:5: note: candidate template ignored: couldn't infer template argument 'N'

    Map( typename NVector<N,T>::type const & vector,  Mapper mapper)

    ^

<source>:31:5: note: candidate template ignored: couldn't infer template argument 'T'

    Map(typename NVector<1,T>::type const & vector, Mapper mapper)

    ^

<source>:49:34: error: no matching function for call to 'Map'

    NVector<2,double>::type db = Map(b,[](int x)->int{return (double)x;});

                                 ^~~

<source>:20:5: note: candidate template ignored: couldn't infer template argument 'N'

    Map( typename NVector<N,T>::type const & vector,  Mapper mapper)

    ^

<source>:31:5: note: candidate template ignored: couldn't infer template argument 'T'

    Map(typename NVector<1,T>::type const & vector, Mapper mapper)

    ^

2 errors generated.

Compiler returned: 1

Estou supondo que o compilador não seja inteligente o suficiente (ou o padrão não especifique como) para descobrir o parâmetro N por dedução. Existe uma maneira de conseguir isso?

Anteriormente, eu tinha esse trabalho, mas de uma maneira diferente, na verdade derivando de std :: vector, mas não gosto dessa solução, pois seria bom trabalhar com o código existente no momento sem precisar introduzir um novo tipo de invólucro.

/// define recursive case
template <int N, typename T>
struct NVector : std::vector<NVector<N-1,T>>;
/// define termination case
template <typename T> 
struct NVector<1, T> : public std::vector<T>;

código ao vivo em https://godbolt.org/z/AMxpuj


Diretamente da minha cabeça - você deve usar o tipo de modelo dedutível Tcomo argumento e usarenable_if
bartop em

Respostas:


3

Você não pode deduzir de um typedef - especialmente um typedef declarado em uma classe auxiliar - porque não há como o compilador executar o mapeamento reverso de um tipo para combinações de argumentos.

(Considere que, no caso geral, isso é impossível, pois alguém pode se especializar struct NVector<100, float> { using type = std::vector<char>; };e o compilador não tem como saber se isso é pretendido.)

Para ajudar o compilador, você pode definir o mapeamento reverso:

template<class T> struct NVT { static constexpr auto D = 0; using V = T; };
template<class T> struct NVT<std::vector<T>> : NVT<T> {
    static constexpr auto D = NVT<T>::D + 1;
};

Possível uso (C ++ 17, mas é fácil de traduzir para dialetos arcaicos):

template<class NV, class Mapper>
auto Map(NV const& vector, Mapper mapper) {
    static constexpr auto N = NVT<NV>::D;
    using T = typename NVT<NV>::V;
    if constexpr (N == 0)
        return mapper(vector);
    else
    {
        typename MapResult<N,T,Mapper>::vector_type out;
        for (auto const& x : vector)
            out.push_back(Map(x, mapper));
        return out;
    }
}

Isso parece ótimo. Qual é a transformação para transformá-lo em C ++ 11 (sem if constexpr). Existe uma maneira genérica de fazer isso? Preso com o compilador antigo :(
bradgonesurfing

1
@bradgonesurfing Crie struct com especialização parcial para N==0e para outros
bartop

Obrigado pela dica. Trabalho. godbolt.org/z/PdqcBu
bradgonesurfing

Agradável. Aqui está como eu fiz isso no C ++ 11: godbolt.org/z/bNzFk3 - é incrível a rapidez com que as habilidades para escrever em versões antigas ficam enferrujadas!
ecatmur 29/01

Isso é melhor do que a minha versão. Eu realmente não precisava criar as estruturas. Sim, se constexpr é super agradável. Pena que não posso usá-lo. Com alguns ajustes, consegui o código funcionando até o VS2010. Golden :) !!
bradgonesurfing

2

Como já foi apontado em outras respostas, o problema aqui é que o especificador de nome aninhado em um ID qualificado é um contexto não deduzido [temp.deduct.type] /5.1 . Outras respostas também já apresentaram várias maneiras diferentes de fazer sua abordagem original funcionar. Gostaria de dar um passo atrás e considerar o que realmente você deseja fazer.

Todos os seus problemas decorrem do fato de você estar tentando trabalhar em termos do modelo auxiliar NVector. O único objetivo deste modelo auxiliar parece ser calcular uma especialização de aninhado std::vector. O único objetivo do modelo auxiliar MapResultparece ser calcular a especialização de aninhado std::vectorque seria necessário para capturar o resultado da aplicação de sua mapperfunção arbitrária a cada elemento da estrutura do vetor de entrada aninhado. Nada obriga a expressar seu Mapmodelo de função em termos desses modelos auxiliares. De fato, a vida é muito mais simples se nos livrarmos deles. Tudo o que você realmente queria fazer era aplicar uma mapperfunção arbitrária a cada elemento de uma std::vectorestrutura aninhada . Então, vamos fazer isso:

template <typename T, typename Mapper>
auto Map(std::vector<T> const& vector, Mapper&& mapper) -> std::vector<decltype(mapper(std::declval<T>()))>
{
    std::vector<decltype(mapper(std::declval<T>()))> out;
    out.reserve(vector.size());
    for (auto& v : vector)
        out.push_back(mapper(v));
    return out;
}

template <typename T, typename Mapper>
auto Map(std::vector<std::vector<T>> const& vector, Mapper&& mapper) -> std::vector<decltype(Map(std::declval<std::vector<T>>(), mapper))>
{
    std::vector<decltype(Map(std::declval<std::vector<T>>(), mapper))> out;
    out.reserve(vector.size());
    for (auto& v : vector)
        out.push_back(Map(v, mapper));
    return out;
}

exemplo de trabalho aqui

Basta soltar os tipos de retorno à direita se você puder usar o C ++ 14 ou mais recente.


Se o que você realmente deseja fazer é apenas armazenar e trabalhar em uma matriz n D, considere que uma estrutura de aninhado std::vectornão é necessariamente a maneira mais eficiente de fazer isso. A menos que você precise que cada subvetor tenha um tamanho potencialmente diferente, não há razão para aumentar exponencialmente o número de alocações de memória dinâmica que você executa com o número de dimensões e seguir o caminho de cada elemento. Basta usar um std::vectorpara conter todos os elementos da matriz n D e definir um mapeamento entre os índices lógicos dos elementos n D e o índice de armazenamento linear 1D, por exemplo, de maneira semelhante ao sugerido nesta resposta. Fazer isso não será apenas mais eficiente do que os vetores de aninhamento, mas também permite alterar facilmente o layout da memória em que seus dados são armazenados. Além disso, como o armazenamento subjacente é uma matriz linear simples, a iteração sobre todos os elementos pode ser feita usando apenas um loop simples e a resposta à sua pergunta sobre o mapeamento de um intervalo de elementos para outro seria simplesmente std::transform


Desculpe, mas você perdeu o que estou tentando fazer. Não quero escrever funções N Map para os N níveis de aninhamento que preciso oferecer suporte e, em seguida, preciso escrever outra quando achar que preciso (N + 1) de níveis de suporte. Veja a resposta de stackoverflow.com/a/59965129/158285
bradgonesurfing

@bradgonesurfing Meu entendimento era que você deseja aplicar uma função de mapeamento a uma estrutura aninhada arbitrariamente std::vectors. A abordagem acima faz exatamente isso e funciona para qualquer N !? Existem duas sobrecargas, uma correspondendo ao caso de um vetor que contém outro vetor e levando à recursão em um nível inferior, e outra tratando o caso base em que a recursão para…
Michael Kenzel

Desculpe meu erro. Eu não li direito. Obrigado
bradgonesurfing 29/01

1
@bradgonesurfing Expandi o exemplo para incluir casos de teste para um vetor aninhado de 3 vias para demonstrar que ele funciona: godbolt.org/z/ksyn5k ;)
Michael Kenzel

1
@bradgonesurfing ok, bem, nesse caso, você vai querer ir com os vetores aninhados, eu acho. Eu apenas pensei em mencionar isso por precaução.
Michael Kenzel 29/01

1

Você não precisa NVectordefinir MapResulte Map.

template <int N, typename T>
struct NVector{
    typedef std::vector<typename NVector<N-1,T>::type> type;
};
template <typename T> struct NVector<1,T> {
    typedef std::vector<T> type;
};

template <typename T, typename Mapper>
struct MapResult {
    typedef decltype(std::declval<Mapper>()(std::declval<T>())) type;
};

template <typename T, typename Mapper>
struct MapResult<std::vector<T>, Mapper> {
    typedef std::vector<typename MapResult<T, Mapper>::type> type;
};

template <typename T, typename Mapper, typename Result = typename MapResult<T, Mapper>::type>
Result Map(T const& elem, Mapper&& mapper)
{
    return mapper(elem);
}

template <typename T, typename Mapper, typename Result = typename MapResult<std::vector<T>, Mapper>::type>
Result Map(std::vector<T> const& vector, Mapper&& mapper)
{
    Result out;
    out.reserve(vector.size());
    for (auto& v : vector)
        out.push_back(Map(v, mapper));
    return out;
}

Corrigi alguns erros que você tinha no código. Agora compila. Ótima solução.
bradgonesurfing 29/01

Infelizmente o VS2010 compilador (sim eu tenho que) não suporta argumentos de modelo padrão em funções
bradgonesurfing

Mas facilmente corrigível. Os parâmetros padrão do modelo eram apenas açúcar para impedir a colar da cópia. Isso funciona no VS2010 (para todas as almas pobres que precisam) gist.github.com/bradphelan/da494160adb32138b46aba4ed3fff967
bradgonesurfing

0

Em geral typename NVector<N,T>::type, não permite deduzir N,Tporque pode haver muitas instâncias de um modelo que produzem o mesmo tipo aninhado.

Sei que você escreveu um mapeamento 1: 1, mas o idioma não exige, e, portanto, não há suporte para trabalhar com versões anteriores dessa maneira. Afinal, você escreveu typename NVector<N,T>::type , mas o que você está realmente passando é std::vector<std::vector<int>>ou o que quer. Não existe uma maneira geral de recuperá-lo.

A solução simples é usar o NVector como um tipo de valor, e não apenas como uma maneira de produzir typedefs de vetor.

template <int N, typename T>
struct NVector{
    using nested = std::vector<NVector<N-1,T>>;
    nested vec;
};
template <typename T> struct NVector<1,T> {
    using nested = std::vector<T>;
    nested vec;
};

altere Map e MapResult para trabalhar diretamente em termos de NVector<N,T>, o que permite a dedução de tipo, como de costume. Por exemplo, o mapa geral se torna

template <int N, typename T, typename Mapper>
typename MapResult<N,T,Mapper>::vector_type 
    Map(NVector<N,T> const & vector,  Mapper mapper)
{
    typename MapResult<N,T,Mapper>::vector_type out;
    for(auto i = vector.vec.begin(); i != vector.vec.end(); i++){
        out.vec.push_back(Map(*i,mapper));
    }
    return out;
}

Finalmente, você precisa declarar suas variáveis ​​locais como NVector<1,int>não ::type, e infelizmente os inicializadores se tornam um pouco mais feios, pois você precisa envolver um extra {}em cada nível. Você sempre pode escrever um construtor para NVectorcontornar isso, no entanto.

Ah, e considere usar em std::transformvez de escrever esse loop manualmente.


O mapeamento do OP na verdade não é 1: 1, pois eles não proíbem Tser do tipo std::vector.
n314159 29/01

0

Você pode usar uma especialização parcial para deduzir N para trás, por assim dizer.

#include <iostream>
#include <vector>

template <typename T, int depth = 0>
struct get_NVector_depth {
    static constexpr int value = depth;
};

template <typename T, int depth>
struct get_NVector_depth<std::vector<T>, depth> {
    static constexpr int value = get_NVector_depth<T, depth+1>::value;
};

int main() {
    std::cout << get_NVector_depth<std::vector<std::vector<int>>>::value;
    std::cout << get_NVector_depth<std::vector<int>>::value;
}

Isso poderia ser usado com o SFINAE para fazer algo como

template <typename T, typename Mapper, std::enable_if_t<get_NVector_depth<T>::value == 1, int> = 0>
typename MapResult<1,T,Mapper>::vector_type  
    Map(const T& vector, Mapper mapper)

0

É perfeitamente correto que o compilador não tente adivinhar o que você quer dizer, porque é ambíguo. Deseja chamar a função com NVector<2, int>ou NVector<1, std::vector<int>>? Ambos são totalmente válidos e dariam a você o mesmo typemembro typedef.

Sua solução anterior funcionou, já que você provavelmente passou o vetor nesse tipo (então o argumento era do tipo NVector<2, int>e a partir daí é fácil deduzir os parâmetros corretos do modelo). Você tem três possibilidades na minha opinião:

  1. Enrole o std::vectornovo em seu tipo personalizado. Mas eu faria isso não com herança, mas apenas com um membro e conversão implícita no tipo desse membro.
  2. Adicione algum tipo de parâmetro de tag ( Nvector<N,T>faria) que desambigua a chamada.
  3. Ligue com argumentos explícitos do modelo.

Eu acho que o terceiro é o mais fácil e claro.


0

Te Nnão são dedutíveis em:

template <int N, typename T, typename Mapper>
typename MapResult<N,T,Mapper>::vector_type 
    Map(typename NVector<N,T>::type const & vector,  Mapper mapper)

Em vez disso, você pode fazer:

// final inner transformation
template <typename T, typename Mapper>
auto Map(const std::vector<T>& v, Mapper mapper)
-> std::vector<typename std::decay<decltype(mapper(*std::begin(v)))>::type>
{
    std::vector<typename std::decay<decltype(mapper(*std::begin(v)))>::type> ret;
    ret.reserve(v.size());
    std::transform(std::begin(v), std::end(v), std::back_inserter(ret), mapper);
    return ret;
}

// recursive call
template <typename T, typename Mapper>
auto Map(const std::vector<std::vector<T>>& v, Mapper mapper)
-> std::vector<typename std::decay<decltype(Map(*std::begin(v), mapper))>::type>
{
    std::vector<typename std::decay<decltype(Map(*std::begin(v), mapper))>::type> ret;
    ret.reserve(v.size());
    std::transform(std::begin(v),
                   std::end(v),
                   std::back_inserter(ret),
                   [&](const std::vector<T>& inner){ return Map(inner, mapper);});
    return ret;
}

Demo

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.