Prefere algoritmos a loops escritos à mão?


10

Qual das seguintes opções você acha mais legível? O loop escrito à mão:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Ou a chamada do algoritmo:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Gostaria de saber se std::for_eachrealmente vale a pena, dado um exemplo tão simples já requer tanto código.

Quais são os seus pensamentos em relação a este assunto?


2
Para C ++, a resposta é bastante óbvia. Mas em outros idiomas, ambos são quase iguais (por exemplo, Python:, map(bar.process, vec)embora o mapa para efeitos colaterais seja desencorajado e as compreensões de lista / expressões geradoras sejam recomendadas sobre o mapa).

5
E também há BOOST_FOREACH...
Benjamin Bannier

Então, o item no STL efetivo não o convenceu? stackoverflow.com/questions/135129/…
Job

Respostas:


18

Há uma razão pela qual as lambdas foram introduzidas, e é porque até o Comitê Padrão reconhece que a segunda forma é péssima. Use o primeiro formulário, até obter suporte para C ++ 0x e lambda.


2
O mesmo raciocínio pode ser usado para justificar a segunda forma: o Comitê de Padrões reconheceu que era tão superior que eles introduziram lambdas para torná-la ainda melhor. :-p (+1 no entanto)
Konrad Rudolph

Eu não acho que o segundo é tão ruim assim. Mas pode ser melhor. Por exemplo, você deliberadamente tornou o objeto Bar mais difícil de usar, não adicionando operator (). Ao usar isso, ele simplifica bastante o ponto de chamada no loop e torna o código arrumado.
Martin York

Nota: o suporte ao lambda foi adicionado devido a duas queixas principais. 1) O padrão padrão necessário para passar parâmetros para os functores. 2) Não tendo o código no ponto de chamada. Seu código não satisfaz nenhum deles, pois você está apenas chamando um método. Portanto, 1) nenhum código extra para passar os parâmetros 2) O código já não está no ponto de chamada, portanto, o lambda não melhorará a manutenção do código. Então, sim, lambda é uma ótima adição. Este não é um bom argumento para eles.
Martin York

9

Sempre use a variante que descreve melhor o que você pretende fazer. Isso é

Para cada elemento xem vec, faça bar.process(x).

Agora, vamos examinar os exemplos:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Temos um for_eachlá também - yippeh . Temos o [begin; end)alcance em que queremos operar.

Em princípio, o algoritmo era muito mais explícito e, portanto, preferível a qualquer implementação escrita à mão. Mas então ... Binders? Memfun? Basicamente C ++ interna de como se apossar de uma função de membro? Para a minha tarefa, eu não me importo com eles! Também não quero sofrer com essa sintaxe detalhada e assustadora.

Agora a outra possibilidade:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

É verdade que esse é um padrão comum para reconhecer, mas ... criar iteradores, repetir, incrementar, desreferenciar. Essas também são todas as coisas de que não me importo para realizar minha tarefa.

É certo que parece melhor do que a primeira solução (pelo menos, o corpo do loop é flexível e bastante explícito), mas ainda assim, não é realmente tão bom assim. Usaremos este se não tivermos uma possibilidade melhor, mas talvez tenhamos ...

Uma maneira melhor?

Agora de volta para for_each. Não seria ótimo dizer literalmente for_each e ser flexível na operação a ser realizada também? Felizmente, desde C ++ 0x lambdas, estamos

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Agora que encontramos uma solução abstrata e genérica para muitas situações relacionadas, vale a pena notar que, neste caso em particular, existe um favorito nº 1 absoluto :

foreach(const Foo& x, vec) bar.process(x);

Realmente não pode ficar muito mais claro que isso. Felizmente, C ++ 0x get de uma sintaxe semelhante built-in !


2
Disse "sintaxe semelhante" sendo for (const Foo& x : vec) bar.process(x);, ou usando, const auto&se quiser.
Jon Purdy

const auto&é possível? Não sabia disso - ótimas informações!
Dario

8

Porque isso é tão ilegível?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}

4
Desculpe, mas por alguma razão diz "censurado" onde acredito que seu código deveria estar.
Mateen Ulhaq

6
Seu código fornece um aviso ao compilador, porque comparar um intcom um size_té perigoso. Além disso, a indexação não funciona com todos os contêineres, por exemplo std::set.
Fredoverflow

2
Esse código funcionará apenas para contêineres com um operador de indexação, dos quais existem poucos. Ele amarra o algoritmo de forma desinteressada - e se um mapa <> fosse melhor? O loop for original e for_each não serão afetados.
JBWilkinson

2
E quantas vezes você cria código para um vetor e decide trocá-lo por um mapa? Como se projetar para isso fosse a prioridade das línguas.
Martin Beckett

2
A iteração sobre coleções por índice é desencorajada porque algumas coleções têm desempenho fraco na indexação, enquanto todas as coleções se comportam bem ao usar um iterador.
slaphappy

3

em geral, a primeira forma é legível para praticamente qualquer pessoa que saiba o que é um loop for, não importa o que eles tenham.

também em geral, o segundo não é tão legível: é fácil entender o que for_each está fazendo, mas se você nunca viu std::bind1st(std::mem_fun_ref, posso imaginar que é difícil entender.


2

Na verdade, mesmo com C ++ 0x, não tenho certeza se for_eachvai receber muito amor.

for(Foo& foo: vec) { bar.process(foo); }

É muito mais legível.

A única coisa que eu não gosto nos algoritmos (em C ++) é que eles raciocinam em iteradores, produzem declarações muito detalhadas.


2

Se você tivesse escrito a barra como um functor, seria muito mais simples:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Aqui o código é bastante legível.


2
É bem legível, além do fato de que você acabou de escrever um functor.
Winston Ewert

Veja aqui por que eu acho que esse código é pior.
SBI

1

Eu prefiro o último porque é arrumado e limpo. Na verdade, faz parte de muitas outras linguagens, mas em C ++ faz parte da biblioteca. Não é realmente importante.


0

Agora, esse problema não é específico para C ++, eu diria.

A questão é que passar algum functor para outra função não visa substituir os loops .
Supõe-se que simplifique certos padrões, que se tornam muito naturais com a programação funcional, como visitante e observador , apenas para citar dois, que vêm à minha mente imediatamente.
Em linguagens que não possuem funções de primeira ordem (Java provavelmente é o melhor exemplo), essas abordagens sempre exigem a implementação de uma determinada interface, que é bastante verbal e redundante.

Um uso comum que vejo muito em outros idiomas seria:

someCollection.forEach(someUnaryFunctor);

A vantagem disso é que você não precisa saber como someCollectioné realmente implementado ou o que someUnaryFunctorfaz. Tudo que você precisa saber é que seu forEachmétodo irá iterar todos os elementos da coleção, passando-os para a função especificada.

Pessoalmente, se você estiver na posição de ter todas as informações sobre a estrutura de dados que deseja iterar e todas as informações sobre o que você deseja fazer em cada etapa da iteração, uma abordagem funcional está complicando demais as coisas, especialmente em um idioma, onde isso é aparentemente bastante entediante.

Além disso, você deve ter em mente que a abordagem funcional é mais lenta, porque você tem muitas chamadas, que têm um certo custo, que você não tem em um loop for.


11
"Além disso, você deve ter em mente que a abordagem funcional é mais lenta, porque você tem muitas chamadas, que têm um certo custo, que você não tem em um loop for". ? Esta declaração não é suportada. A abordagem funcional não é necessariamente mais lenta, especialmente porque pode haver otimizações ocultas. E mesmo que você entenda completamente seu loop, ele provavelmente parecerá mais limpo em sua forma mais abstrata.
Andres F.

@ André F: Então isso parece mais limpo? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));E é mais lento no tempo de execução ou no tempo de compilação (se você tiver sorte). Não vejo por que substituir estruturas de controle de fluxo por chamadas de função melhora o código. Sobre qualquer alternativa apresentada aqui é mais legível.
Back2dos

0

Acho que o problema aqui é que for_eachnão é realmente um algoritmo, é apenas uma maneira diferente (e geralmente inferior) de escrever um loop escrito à mão. dessa perspectiva, ele deve ser o algoritmo padrão menos usado e, sim, você provavelmente também pode usar um loop for. no entanto, o conselho no título ainda permanece, pois existem vários algoritmos padrão personalizados para usos mais específicos.

Onde houver um algoritmo que faça com mais precisão o que você deseja, sim, você deve preferir algoritmos a loops escritos à mão. exemplos óbvios aqui estão classificando com sortou stable_sortou pesquisar com lower_bound, upper_boundou equal_range, mas a maioria deles tem alguma situação em que se encontram preferível usar mais de um loop codificado mão.


0

Para este pequeno trecho de código, ambos são igualmente legíveis para mim. Em geral, acho que os loops manuais são propensos a erros e prefiro usar algoritmos, mas isso realmente depende de uma situação concreta.

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.