Com quase todo o código que escrevo, frequentemente estou lidando com problemas de redução de conjuntos em coleções que acabam resultando em condições ingênuas "se" dentro delas. Aqui está um exemplo simples:
for(int i=0; i<myCollection.size(); i++)
{
if (myCollection[i] == SOMETHING)
{
DoStuff();
}
}
Com linguagens funcionais, posso resolver o problema reduzindo a coleção para outra coleção (facilmente) e, em seguida, realizar todas as operações em meu conjunto reduzido. Em pseudocódigo:
newCollection <- myCollection where <x=true
map DoStuff newCollection
E em outras variantes C, como C #, eu poderia reduzir com uma cláusula where como
foreach (var x in myCollection.Where(c=> c == SOMETHING))
{
DoStuff();
}
Ou melhor (pelo menos aos meus olhos)
myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));
Reconheço que estou fazendo muita mistura de paradigmas e estilo subjetivo / baseado em opinião, mas não posso deixar de sentir que estou perdendo algo realmente fundamental que poderia me permitir usar essa técnica preferida com C ++. Alguém poderia me esclarecer?
std::copy_if
, mas as seleções não são preguiçosasif
interior de um quefor
você mencionou não é apenas funcionalmente equivalente aos outros exemplos, mas também provavelmente seria mais rápido em muitos casos. Também para alguém que afirma gostar de estilo funcional, o que você está promovendo parece ir contra o querido conceito de pureza da programação funcional, uma vez queDoStuff
claramente tem efeitos colaterais.Respostas:
IMHO é mais simples e mais legível usar um loop for com um if dentro dele. No entanto, se isso for irritante para você, você pode usar um
for_each_if
como este abaixo:Caso de uso:
Demonstração ao vivo
fonte
find_if
efind
se eles funcionam em faixas ou pares de iteradores. (Existem algumas exceções, comofor_each
efor_each_n
). A maneira de evitar escrever novos algos para cada espirro é usar operações diferentes com os algoritmos existentes, por exemplo, em vez defor_each_if
incorporar a condição ao chamável passado parafor_each
, por exemplofor_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Boost fornece intervalos que podem ser usados com base em intervalo para. Os intervalos têm a vantagem de não copiar a estrutura de dados subjacente, eles apenas fornecem uma 'visualização' (ou seja
begin()
,end()
para o intervalo eoperator++()
,operator==()
para o iterador). Isso pode ser do seu interesse: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.htmlfonte
is_even
=>condition
,input
=>myCollection
etc.filtered()
para você - dito isso, é melhor usar uma biblioteca com suporte do que algum código ad-hoc.Em vez de criar um novo algoritmo, como faz a resposta aceita, você pode usar um existente com uma função que aplique a condição:
Ou se você realmente deseja um novo algoritmo, pelo menos reutilize
for_each
lá em vez de duplicar a lógica de iteração:fonte
std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });
é totalmente mais simples do quefor (Iter x = first; x != last; x++) if (p(x)) op(x);}
?std::for_each(std::execution::par, first, last, ...);
Quão fácil é adicionar essas coisas a um loop escrito à mão?A ideia de evitar
construções como um antipadrão é muito amplo.
É perfeitamente normal processar vários itens que correspondem a uma determinada expressão de dentro de um loop, e o código não pode ficar muito mais claro do que isso. Se o processamento ficar muito grande para caber na tela, esse é um bom motivo para usar uma sub-rotina, mas ainda assim o condicional é melhor colocado dentro do loop, ou seja,
é amplamente preferível a
Ele se torna um antipadrão quando apenas um elemento corresponderá, porque então seria mais claro pesquisar primeiro o elemento e executar o processamento fora do loop.
é um exemplo extremo e óbvio disso. Mais sutil e, portanto, mais comum, é um padrão de fábrica como
Isso é difícil de ler, porque não é óbvio que o código do corpo será executado apenas uma vez. Nesse caso, seria melhor separar a pesquisa:
Ainda existe um
if
dentro de afor
, mas a partir do contexto fica claro o que ele faz, não há necessidade de alterar este código a menos que a pesquisa mude (por exemplo, para amap
), e é imediatamente claro quecreate_object()
é chamado apenas uma vez, porque é não dentro de um loop.fonte
for( range ){ if( condition ){ action } }
estilo torna mais fácil ler as coisas uma por vez e usa apenas o conhecimento das construções básicas da linguagem.for(...) if(...) { ... }
muitas vezes é a melhor escolha (é por isso que qualifiquei a recomendação de dividir a ação em uma sub-rotina).for(…)if(…)…
se fosse o único lugar que a pesquisa ocorreu.Aqui está um rápido relativamente mínimo
filter
função .É preciso um predicado. Ele retorna um objeto de função que leva um iterável.
Ele retorna um iterável que pode ser usado em um
for(:)
loop.Eu peguei atalhos. Uma biblioteca real deve fazer iteradores reais, não o
for(:)
pseudo-fascades qualificantes que eu fiz.No ponto de uso, tem a seguinte aparência:
o que é muito bom e imprime
Exemplo vivo .
Há uma proposta de adição ao C ++ chamada Rangesv3 que faz esse tipo de coisa e muito mais.
boost
também tem faixas / iteradores de filtro disponíveis. boost também tem ajudantes que tornam a escrita do texto acima muito mais curta.fonte
Um estilo que é usado o suficiente para ser mencionado, mas não foi mencionado ainda, é:
Vantagens:
DoStuff();
quando a complexidade da condição aumenta. Logicamente,DoStuff();
deve estar no nível superior dofor
loop, e está.SOMETHING
s da coleção, sem exigir que o leitor verifique se não há nada após o fechamento}
doif
bloco.Desvantagens:
continue
, como outras instruções de controle de fluxo, é mal utilizado de maneiras que levam a códigos difíceis de seguir, tanto que algumas pessoas se opõem a qualquer uso deles: há um estilo válido de codificação que alguns seguem que evitacontinue
, que evitabreak
outros que não em aswitch
, que evitareturn
outros que não no final de uma função.fonte
for
loop que se estende por muitas linhas, um "se não, continue" de duas linhas é muito mais claro, lógico e legível. Dizendo imediatamente "pule isto se" depois que afor
instrução for bem lida e, como você disse, não indente os aspectos funcionais restantes do loop. Se ocontinue
estiver mais abaixo, no entanto, alguma clareza será sacrificada (ou seja, se alguma operação sempre será realizada antes daif
instrução).Parece muito com uma
for
compreensão específica de C ++ para mim. Para você?fonte
auto const
não tem qualquer relação com a ordem de iteração. Se você pesquisar com base em intervalosfor
, verá que basicamente faz um loop padrão debegin()
paraend()
com desreferenciação implícita. Não há como quebrar as garantias de pedido (se houver) do contêiner sendo iterado; teria sido ridicularizado na face da Terrastd::future
s,std::function
s, mesmo aqueles fechamentos anônimos são muito bons em C ++ na sintaxe; cada linguagem tem sua própria linguagem e, ao incorporar novos recursos, tenta fazê-los imitar a antiga e conhecida sintaxe.Se DoStuff () fosse dependente de i de alguma forma no futuro, eu proporia esta variante garantida de mascaramento de bits sem ramificação.
Onde popcount é qualquer função que faz uma contagem da população (contagem do número de bits = 1). Haverá alguma liberdade para colocar restrições mais avançadas com i e seus vizinhos. Se isso não for necessário, podemos remover o loop interno e refazer o loop externo
seguido por um
fonte
Além disso, se você não se importar em reordenar a coleção, std :: partition é barato.
fonte
std::partition
reordena o contêiner.Estou pasmo com a complexidade das soluções acima. Eu ia sugerir um simples,
#define foreach(a,b,c,d) for(a; b; c)if(d)
mas tem alguns déficits óbvios, por exemplo, você deve se lembrar de usar vírgulas em vez de ponto-e-vírgulas em seu loop, e você não pode usar o operador vírgula ema
ouc
.fonte
Outra solução caso os i: s sejam importantes. Este constrói uma lista que preenche os índices para os quais chamar doStuff (). Mais uma vez, o ponto principal é evitar a ramificação e trocá-la por custos aritméticos canalizáveis.
A linha "mágica" é a linha de carregamento do buffer que calcula aritmeticamente se deve manter o valor e permanecer na posição ou contar a posição e adicionar valor. Portanto, trocamos um ramo potencial por algumas lógicas e aritméticas e talvez algumas ocorrências de cache. Um cenário típico em que isso seria útil seria se doStuff () fizesse uma pequena quantidade de cálculos pipelináveis e qualquer ramificação entre as chamadas pudesse interromper esses pipelines.
Então, apenas faça um loop no buffer e execute doStuff () até chegarmos a cnt. Desta vez, teremos o i atual armazenado no buffer para que possamos usá-lo na chamada de doStuff () se precisarmos.
fonte
Pode-se descrever seu padrão de código como a aplicação de alguma função a um subconjunto de um intervalo, ou em outras palavras: aplicá-lo ao resultado da aplicação de um filtro a todo o intervalo.
Isso é possível da maneira mais direta com a biblioteca de intervalos-v3 de Eric Neibler ; embora seja um pouco desagradável, porque você deseja trabalhar com índices:
Mas se você estiver disposto a renunciar aos índices, você obterá:
que é melhor IMHO.
PS - A biblioteca de intervalos está indo principalmente para o padrão C ++ em C ++ 20.
fonte
Vou apenas mencionar Mike Acton, ele definitivamente diria:
Se você tiver que fazer isso, você terá um problema com seus dados. Classifique seus dados!
fonte