Prática recomendada para "continuar" de dentro de um loop aninhado?


8

Aqui está uma amostra simplificada. Basicamente, ele verifica uma string de uma lista de strings. Se a verificação for aprovada, ela removerá a string ( filterStringOut(i);) e não será mais necessário continuar com outras verificações. Assim, continuepara a próxima string.

void ParsingTools::filterStrings(QStringList &sl)
{
    /* Filter string list */
    QString s;
    for (int i=0; i<sl.length(); i++) {
        s = sl.at(i);

        // Improper length, remove
        if (s.length() != m_Length) {
            filterStringOut(i);
            continue; // Once removed, can move on to the next string
        }          
        // Lacks a substring, remove
        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */ 
            }
        }
        // Contains a substring, remove
        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */ 
            }
        } 
    }
}

Como se deve continuar o loop externo de dentro de um loop aninhado?

Meu melhor palpite é usar gotoe colocar um rótulo no final do loop externo. Isso me levou a fazer essa pergunta, dado o quão tabu gotopode ser.

No bate-papo do c ++ IRC, foi sugerido que eu coloque os forloops nas funções bool, que retornam true se uma verificação for aprovada. portanto

if ( containsExclude(s)) continue;
if (!containsInclude(s)) continue;

ou simplesmente crie um booleano local, defina-o como true break, verifique bool e continue se true.

Como estou usando isso em um analisador, na verdade, preciso priorizar o desempenho neste exemplo. É uma situação em que gotoainda é útil ou é necessário reestruturar meu código?



3
O C ++ não possui quebras rotuladas, portanto, a prática canônica e aceita é imitá-las via goto, apesar de sua má reputação. Não tema nomes - tema conceitos.
Kilian Foth

2
@ Akiva: então você realmente mediu a diferença de desempenho? E só porque você ouviu "goto" é uma maneira aceitável de romper um loop aninhado não implica que a alternativa de introduzir uma função com um nome claro e conciso não seja mais legível.
Doc Brown

3
@Akiva: o benchmarking é bastante simples, você não precisa de ferramentas ou habilidades especiais: configure um pequeno programa que chame essa função com alguns dados de teste em loop várias vezes (talvez vários milhões de vezes) e meça o tempo de execução com um cronômetro. Faça o mesmo com o código de limpeza. Aposto que a diferença será insignificante (é claro, não se esqueça de usar otimizações do compilador).
Doc Brown

5
Por que você está reinventando a roda ?
Alexander - Restabelece Monica

Respostas:


16

Não aninhe: converta para funções. E faça com que essas funções retornem truese eles executarem sua ação e as etapas subsequentes puderem ser ignoradas; falsede outra forma. Dessa forma, você evita completamente todo o problema de como sair de um nível, continuar dentro de outro, etc. como você apenas encadeia as chamadas ||(isso pressupõe que o C ++ interrompe o processamento de uma expressão em um true; acho que sim).

Portanto, seu código pode acabar parecido com o seguinte (não escrevo C ++ há anos, portanto, provavelmente contém erros de sintaxe, mas deve fornecer uma idéia geral):

void ParsingTools::filterStrings(QStringList &sl)
{
    QString s;
    for (int i=0; i<sl.length(); i++) {
        s = sl.at(i);

        removeIfImproperLength(s, i) ||
        removeIfLacksRequiredSubstring(s, i) ||
        removeIfContainsInvalidSubstring(s, i);
    }
}

bool removeIfImproperLength(QString s, int i) {
    if (s.length() != m_Length) 
    {
        filterStringOut(i);
        return true;
    }
    return false;
}          

bool removeIfLacksSubstring(QString s, int i) {
    for (int j=0; j<m_Include.length(); j++) {
        if (!s.contains(m_Include.at(j))) { 
            filterStringOut(i);
            return true; 
        }
    }

    return false;
}

bool removeIfContainsInvalidSubstring(QString s, int i) {
    for (int j=0; j<m_Exclude.length(); j++) {
        if (s.contains(m_Exclude.at(j))) { 
            filterStringOut(i); 
            return true;
        }
    } 

    return false;
}

1
Erro de digitação "reoveIfImproperLength". :)
Neil

5
É melhor que as três funções de verificação de condição permaneçam livres de efeitos colaterais (ou seja, em vez de fazer "remove-if", basta retornar a condição booleana e deixar que o chamador ( ParsingTools::filterStrings) chame a filterStringOut(i)função, conforme mostrado na resposta de dagnelies.
rwong

Então você está usando a semântica de chamada de função como base para as instruções de interrupção ausentes do C ++. Muito esperto.
Ryan Reich

13

De uma perspectiva mais detalhada, refatoraria o código para que fique assim ... (em pseudo-código, faz muito tempo que toquei em C ++)

void filterStrings(sl)
{
    /* Filter string list */
    for (int i=0; i<sl.length(); i++) {
        QString s = sl.at(i);
        if(!isProperString(s)) {
           filterStringOut(i);
        }
     }
}

bool isProperString(s) {

        if (s.length() != m_Length)
            return false; // Improper length

        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                return false; // Lacks a substring
            }
        }

        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                return false; // Contains a substring
            }
        }

        return true; // all tests passed, it's a proper string
}

Este é o limpador IMHO, porque separa claramente o que constitui uma string adequada e o que você faz quando não é.

Você poderia dar um passo adiante e usar métodos de filtro internos, como myProperStrings = allMyStrings.filter(isProperString)


10

Eu realmente gosto de como @dagnelies começa . Curto e direto ao ponto. Um bom uso da abstração de alto nível. Estou apenas ajustando sua assinatura e evitando um negativo desnecessário.

void ParsingTools::filterStrings(QStringList &sl)
{
    for (int i=0; i<sl.length(); i++) {
        QString s = sl.at(i);
        if ( isRejectString(s) ) {
            filterStringOut(i);
        }
    }
}

No entanto, gosto de como o @DavidArno divide os testes de requisitos como funções individuais. Claro que a coisa toda fica mais longa, mas todas as funções são maravilhosamente pequenas. Seus nomes evitam a necessidade de comentários para explicar o que são. Só não gosto que eles assumam a responsabilidade extra de ligar filterStringOut().

A propósito, sim, o C ++ interromperá a avaliação de uma ||cadeia trueenquanto você não sobrecarregar o ||operador. Isso é chamado de avaliação de curto-circuito . Mas esta é uma micro otimização trivial que você pode ignorar ao ler o código, desde que as funções sejam livres de efeitos colaterais (como as abaixo).

O seguinte deve tornar clara a definição de uma sequência de rejeição sem arrastar você por detalhes desnecessários:

bool isRejectString(QString s) {
    return isDifferentLength(s, m_Length) 
        || sansRequiredSubstring(s, m_Include)
        || hasForbiddenSubstring(s, m_Exclude)
    ;
}

Aliviado da necessidade de chamar filterStringOut()as funções de teste de requisitos, fica mais curto e seus nomes são muito mais simples. Também coloquei tudo o que eles dependem em sua lista de parâmetros para facilitar a compreensão sem olhar para dentro.

bool isDifferentLength(QString s, int length) {
    return ( s.length() != length );
}

bool sansRequiredSubstring(QString s, QStringList &include) {
    for (int j=0; j<include.length(); j++) {
        QString requiredSubstring = include.at(j);
        if ( !s.contains(requiredSubstring) ) { 
            return true; 
        }
    }
    return false;
}

bool hasForbiddenSubstring(QString s, QStringList &exclude) {
    for (int j=0; j<exclude.length(); j++) {
    QString forbiddenSubstring = exclude.at(j);
        if ( s.contains(forbiddenSubstring) ) { 
            return true; 
        }
    }
    return false;
}

Eu adicionei requiredSubstringe forbiddenSubstringpara os humanos. Eles vão te atrasar? Teste e descubra. É mais fácil tornar o código legível realmente rápido, em seguida, tornar o código prematuramente otimizado legível ou realmente rápido.

Se as funções ficarem mais lentas, procure funções inline antes de submeter os humanos a códigos ilegíveis. Novamente, não assuma que isso lhe dará velocidade. Teste.

Acho que você encontrará qualquer um desses itens mais legíveis do que aninhados para loops. Aqueles, combinados com os if, estavam começando a dar a você um verdadeiro anti-padrão de flechas . Acho que a lição aqui é que pequenas funções são uma coisa boa.


1
Embora seja uma combinação de outras duas respostas, isso agrega muito valor. Faça "para humanos", teste o desempenho antes de decidir "ocultar" o código e facilite o teste. Coisas boas!
Carlossierra

1
Na verdade, meu uso do ! isProperStringque isImproperStringfoi intencional. Eu costumo evitar negações nos nomes das funções. Imagine que você precisa verificar se é realmente uma sequência adequada mais tarde, você precisará do !isImproperStringque é IMHO mais propenso a confusão por causa da dupla negação.
Dagnelies

@dagnelies melhor?
Candied_orange 15/05

4

Basta usar um lambda para o predicado e, em seguida, usar o poder dos algoritmos padrão e do curto-circuito. Não há necessidade de qualquer fluxo de controle complicado ou exótico:

void ParsingTools::filterStrings (QStringList& list)
{
    for (int i = list.size(); i--;) {
        const auto& s = list[i];
        auto contains = [&](const QString& x) { return s.contains(x); };
        if (s.size() != m_Length
                || !std::all_of(m_Include.begin(), m_Include.end(), contains)
                || std::any_of(m_Exclude.begin(), m_Exclude.end(), contains))
            filterStringOut(i);
    }
}

1

Também há a opção de tornar o conteúdo do loop externo (aquele que você deseja continuar) um lambda e simplesmente usar return.
É surpreendentemente fácil se você conhece lambdas; você basicamente inicia o interior do loop [&]{e termina com }(); dentro você pode usar return;a qualquer momento para deixá-lo:

void ParsingTools::filterStrings(QStringList &sl)
{
    /* Filter string list */
    QString s;
    for (int i=0; i<sl.length(); i++) {

      [&]{    // start a lamdba defintion

        s = sl.at(i);

        // Improper length, remove
        if (s.length() != m_Length) {
            filterStringOut(i);
            // continue; // Once removed, can move on to the next string
            return; // happily return here, this will continue 
        }          
        // Lacks a substring, remove
        for (int j=0; j<m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */  return;  // happily return here, this will continue the i-loop
            }
        }
        // Contains a substring, remove
        for (int j=0; j<m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) { 
                filterStringOut(i); 
                /* break; and continue; */  return; // happily return here, this will continue the i-loop
            }
        } 

      }()   // close/end the lambda definition and call it

    }
}

3
(1) Para realmente ligar imediatamente para o lambda , no final da chave de fechamento, é preciso fazer a ligação (com um par de parênteses, potencialmente com a lista de argumentos ou sem). (2) todos os três locais que usam continuee breakdevem ser substituídos por return. Seu código parece deixar o primeiro lugar (que usa continue) inalterado, mas isso também deve ser alterado, porque o código está dentro do lambda e a continueinstrução não conseguiu encontrar um escopo que é um loop.
Rwong 5/05

Eu escrevi isso enquanto esperava no sinal vermelho. Corrigido.
Aganju

1

Eu acho que @dganelies tem a idéia certa como ponto de partida, mas acho que consideraria um passo adiante: escreva uma função genérica que possa executar o mesmo padrão para (quase) qualquer contêiner, critério e ação:

template <class Container, class Action, class Condition>
void map_if(Container &container, Action action, Condition cond) {
    for (std::size_t i = 0; i < container.length(); i++) {
        auto s = container.at(i);
        if (cond(s))
            action(i);
    }
}

Você filterStringsapenas definiria os critérios e passaria a ação apropriada:

void ParsingTools::filterStrings(QStringList const &sl)
{
    auto isBad = [&](QString const &s) {

        if (s.length() != m_Length)
            return true;

        for (int j = 0; j < m_Include.length(); j++) {
            if (!s.contains(m_Include.at(j))) {
                return true;
            }
        }

        for (int j = 0; j < m_Exclude.length(); j++) {
            if (s.contains(m_Exclude.at(j))) {
                return true;
            }
        }
        return false;
    };

    map_if(sl, filterStringOut, isBad);
}

Obviamente, existem outras maneiras de abordar esse problema básico também. Por exemplo, usando a biblioteca padrão, você parece querer algo na mesma ordem geral que std::remove_if.


1

Várias respostas sugerem um grande refator do código. Provavelmente, esse não é um caminho ruim, mas eu gostaria de fornecer uma resposta que esteja mais de acordo com a pergunta em si.

Regra # 1: perfil antes de otimizar

Sempre analise os resultados antes de tentar uma otimização. Você pode perder muito tempo se não o fizer.

Dito isto ...

No momento, testei pessoalmente esse tipo de código no MSVC. Os booleanos são o caminho a percorrer. Nomeie o booleano como algo semanticamente significativo containsString.

    ...
    boo containsString = true; // true until proven false
    // Lacks a substring, remove
    for (int j=0; j<m_Include.length(); j++) {
        if (!s.contains(m_Include.at(j))) { 
            filterStringOut(i); 
            /* break; and continue; */ 
            containsString = false;
        }
    }
    if (!containsString)
        continue;

No MSVC (2008), no modo de liberação (configurações típicas do otimizador), o compilador otimizou um loop semelhante até exatamente o mesmo conjunto de códigos de operação da gotoversão. Foi inteligente o suficiente para ver que o valor do booleano estava diretamente ligado ao controle do fluxo e eliminava tudo. Não testei o gcc, mas presumo que ele possa fazer tipos semelhantes de otimização.

Isso tem a vantagem gotode simplesmente não levantar nenhuma preocupação dos puristas que consideram gotoprejudiciais, sem sacrificar o desempenho de uma única instrução.

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.