Uma função recursive_transform para vários tipos Iterable aninhada com implementação std :: variant em C ++

Oct 23 2020

Esta é uma pergunta de acompanhamento para Uma função TransformAll para vários tipos de implementação Iterable aninhada arbitrária em C ++ . O código a seguir é a versão aprimorada com base na resposta de G. Sliepen . Para se adequar às convenções da STL, a função nomeada recursive_transformaqui usa o is_iterableconceito e o is_element_iterableconceito. Além disso, a operação de cópia da entrada é evitada atualizando [_Func](auto element)->autoem [_Func](auto& element)e a parte redundante nesta função lambda ->autofoi removida. Embora o código tenha sido aprimorado, descobri que há alguns casos em que a TransformAllfunção da versão anterior é difícil de lidar. Um desses casos são os intervalos iteráveis ​​aninhados com std::variant. Quero me concentrar neste caso, comostd::vector<std::variant<long double>>. Em primeiro lugar, o conceito adicional is_element_varianté incluído para determinar o tipo de elementos em um contêiner iterável std::variantou não. Acho que pode haver outra implementação melhor para esse is_element_variantconceito. No entanto, o método que pesquisei Como verificar se o argumento do modelo é std :: variant? não lida com isso com conceitos c ++. Prefiro trabalhar com o conceito aqui e o código experimental é o seguinte. Se houver alguma sugestão de como melhorar este is_element_variantconceito, por favor me avise.

template<typename T>
concept is_element_variant = requires(T x)
{
    x.begin()->index();
    x.begin()->valueless_by_exception();
};

A parte da função de modelo recursive_transformque manipula a std::variantestrutura:

template<class T, class _Fn> requires is_iterable<T> && is_element_variant<T>
static T recursive_transform(const T _input, _Fn _Func);       //  Deal with the iterable case which its element is std::variant

template<class T, class _Fn> requires is_iterable<T> && is_element_variant<T>
static inline T recursive_transform(const T _input, _Fn _Func)
{
    T returnObject = _input;
    
    std::transform(_input.begin(), _input.end(), returnObject.begin(), 
        [_Func](typename std::iterator_traits<typename T::iterator>::value_type x)->
        typename std::iterator_traits<typename T::iterator>::value_type
        {
            return std::visit([_Func](auto&& arg) -> typename std::iterator_traits<typename T::iterator>::value_type
                    {
                        return _Func(arg);
                    }, x);
        });
    return returnObject;
}

As outras partes:

template<typename T>
concept is_iterable = requires(T x)
{
    x.begin();      // must have `x.begin()` 
    x.end();        // and `x.end()` 
};

template<typename T>
concept is_element_iterable = requires(T x)
{
    x.begin()->begin();
    x.end()->end();
};

template<class T, class _Fn> requires is_iterable<T>
static T recursive_transform(const T _input, _Fn _Func);       //  Deal with the iterable case like "std::vector<long double>"

template<class T, class _Fn> requires is_iterable<T>
static inline T recursive_transform(const T _input, _Fn _Func)
{
    T returnObject = _input;

    std::transform(_input.begin(), _input.end(), returnObject.begin(), _Func);
    return returnObject;
}

template<class T, class _Fn> requires is_iterable<T> && is_element_iterable<T>
static T recursive_transform(const T _input, _Fn _Func);

template<class T, class _Fn> requires is_iterable<T> && is_element_iterable<T>
static inline T recursive_transform(const T _input, _Fn _Func)
{
    T returnObject = _input;
    std::transform(_input.begin(), _input.end(), returnObject.begin(),
        [_Func](auto& element)
        {
            return recursive_transform(element, _Func);
        }
    );
    return returnObject;
}

int main()
{
    std::vector<long double> testVector1;
    testVector1.push_back(1);
    testVector1.push_back(20);
    testVector1.push_back(-100);
    std::cout << recursive_transform(testVector1, [](long double x)->long double { return x + 1; }).at(0) << std::endl;

    std::vector<long double> testVector2;
    testVector2.push_back(10);
    testVector2.push_back(90);
    testVector2.push_back(-30);

    std::vector<std::vector<long double>> testVector3;
    testVector3.push_back(testVector1);
    testVector3.push_back(testVector2);
    std::cout << recursive_transform(testVector3, [](long double x)->long double { return x + 1; }).at(1).at(1) << std::endl;
    
    std::vector<std::variant<long double>> testVector4;
    testVector4.push_back(1);
    testVector4.push_back(20);
    testVector4.push_back(-100);

    auto operation_to_element = [](long double number) { return number + 2; };

    std::visit([](auto&& arg) {std::cout << arg; },         //  For printing
        recursive_transform(testVector4, operation_to_element).at(0)
    );

    return 0;   
}

Todas as sugestões são bem-vindas.

As informações resumidas:

  • Para qual pergunta é um seguimento?

    Uma função TransformAll para vários tipos de implementação arbitrária aninhada Iterable em C ++

  • Que mudanças foram feitas no código desde a última pergunta?

    • Renomeie a função para recursive_transformque corresponda às convenções da STL.
    • A operação de cópia da entrada é evitada pela atualização auto &element.
    • Remova a parte redundante na função lambda ->auto
  • Por que uma nova revisão está sendo solicitada?

    Acho que o conceito is_element_variantpode ser melhorado e estou ansioso para qualquer sugestão de possíveis formas de melhoria. Além disso, na minha opinião sobre a parte da função de modelo recursive_transformque manipula a std::variantestrutura, a implementação aqui é complexa, existem duas funções lambda aninhadas. Se houver alguma forma de simplificar isso, por favor me avise.

Respostas

2 G.Sliepen Oct 23 2020 at 20:33

Agora você está tornando seus algoritmos mais especializados novamente. Pessoalmente, eu evitaria isso e deixaria a transformação recursiva recursive_transform()e o tratamento da visita na variante para o chamador. Talvez haja maneiras de tornar mais fácil para o chamador fazer isso, mas nesta resposta irei apenas comentar sobre sua implementação.

Seja o mais preciso possível com seus conceitos

Os conceitos que você está usando devem testar exatamente o que você precisa. Em seu código, você não está chamando index()nem valueless_by_exception(), portanto, isso não deve ser testado nos conceitos de que você precisa. Em vez disso, o que você precisa testar é se você pode chamar std::visit()um elemento, assim:

template<typename T>
concept is_element_visitable = requires(T x)
{
    std::visit([](auto){}, *x.begin());
};

Simplifique a maneira como você escreve tipos

Use autoe decltype()onde aplicável para evitar escrever tipos de forma indireta. Isso também tem a vantagem de não exigir que haja iterator_traitsaliases adequados e de outro tipo definidos para os contêineres que podem ser usados. Por exemplo:

template<class T, class Fn> requires is_iterable<T> && is_element_visitable<T>
static inline T recursive_transform(const T input, Fn func)
{
    T result = input;
    
    std::transform(input.begin(), input.end(), result.begin(), 
        [func](auto x) -> decltype(x) {
            return std::visit([_Func](auto&& arg) -> decltype(x) {
                return func(arg);
            }, x);
        }
    );

    return result;
}

Não há necessidade de especificar explicitamente o tipo de x, na melhor das hipóteses é o mesmo que o tipo de argumento que é passado, na pior, você comete um erro que compila sem erros, mas causa uma conversão sutil. E como você deseja retornar um valor que tenha o mesmo tipo de x(para que possamos converter o resultado de func()volta em a std::variant, basta escrever -> decltype(x)como o tipo de retorno final. Você pode fazer o mesmo para o tipo de retorno final do lambda passado para std::visit().

Bem, isso seria verdade, exceto que o exemplo acima é tão compacto porque você está copiando por valor, o que me leva a:

Evite cópias desnecessárias

Eu perdi isso na minha revisão anterior, mas há mais lugares onde você faz uma cópia: sempre que uma função pega um parâmetro por valor, ela é copiada. Portanto, para evitar as cópias dispendiosas de grandes contêineres, certifique-se de passar as entradas tanto quanto possível por const referência , tanto para os parâmetros da função modelada quanto para os parâmetros passados ​​para as funções lambda.

Agora, precisamos garantir que os tipos de retorno finais não se tornem referências. Para fazer isso, você pode usar std::remove_reference. Torna-se um pouco mais confuso, então eu usaria uma usingdeclaração:

template<class T, class Fn> requires is_iterable<T> && is_element_visitable<T>
static inline T recursive_transform(const T &input, Fn func)
{
    using value_type = std::remove_reference<decltype(*input.begin())>::type;
    T result = input;
    
    std::transform(input.begin(), input.end(), result.begin(), 
        [func](const auto &x) -> value_type {
            return std::visit([_Func](auto&& arg) -> value_type {
                return func(arg);
            }, x);
        }
    );

    return result;
}

Remova declarações de encaminhamento redundantes

Cada definição de modelo é precedida por uma declaração de encaminhamento. Em geral, você deve evitar declarações de encaminhamento desnecessárias, pois elas se repetem e permitem diferenças acidentais na declaração de encaminhamento e na definição real. Isso é muito mais importante para os modelos, porque lá a chance de o compilador perceber um conflito é muito menor.

Não use a long doublemenos que você realmente precise dessa precisão extra

Vejo que você usa long doubleconsistentemente em seu código, mas se você não precisa da precisão extra que ele pode ter sobre a double, provavelmente pagará o preço com desempenho inferior. A razão é que no x86 e no x86_64, as long doubleoperações só podem ser feitas com registros e instruções FPU x87 , e não com registros e instruções SSE. Há também uma grande sobrecarga de armazenamento long double.