web-dev-qa-db-ja.com

ネストされたループ内から「続行」するためのベストプラクティスは?

これは簡略化されたサンプルです。基本的に、文字列リストから文字列をチェックします。チェックにパスすると、その文字列(filterStringOut(i);)が削除され、他のチェックを続行する必要がなくなります。したがって、continueは次の文字列になります。

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; */ 
            }
        } 
    }
}

ネストされたループの内側から外側のループをどのように続けるべきですか?

私の推測では、gotoを使用して、外側のループの最後にラベルを配置します。そのため、タブーgotoの可能性を考えると、この質問をするように求められました。

C++ IRCチャットでは、チェックがパスした場合にtrueを返すforループをブール関数に配置することが推奨されました。

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

または、単純にローカルブール値を作成し、それをtrue breakに設定し、ブール値をチェックして、trueの場合は続行します。

これをパーサーで使用しているので、この例では実際にパフォーマンスを優先する必要があります。これはgotoがまだ役立つ状況ですか、それともコードを再構成する必要がある場合ですか?

8
Akiva

ネストしないでください。代わりに関数に変換してください。そして、それらの関数がアクションを実行し、後続のステップをスキップできる場合は、それらの関数がtrueを返すようにします。 falseそれ以外の場合。そうすれば、||で呼び出しをチェーンするだけで、1つのレベルから抜け出し、別のレベルで続行するなどの問題全体を完全に回避できます(これは、C++がtrueで式の処理を停止することを前提としています) ;そうだと思います)。

したがって、コードは次のようになる可能性があります(私は何年もC++を作成していないので、構文エラーが含まれている可能性がありますが、一般的な考え方はわかるはずです)。

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;
}
16
David Arno

より鳥瞰図の観点から、私はこのように見えるようにコードをリファクタリングします...(疑似コードでは、私は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
}
_

これは、適切な文字列を構成するものとそうでない場合に行うことを明確に区別するため、IMHOのクリーナーです。

さらに一歩進んで、myProperStrings = allMyStrings.filter(isProperString)のような組み込みのフィルターメソッドを使用することもできます。

13
dagnelies

私は本当に @ dagneliesの始まり が好きです。短くて要点。高レベルの抽象化の適切な使用。私はそれをシグネチャを微調整し、不必要なネガティブを避けています。

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

ただし、個々の関数として @ DavidArnoが要件テストをどのように実行するか が好きです。確かに全体が長くなりますが、すべての機能は驚くほど小さいです。彼らの名前は、彼らが何であるかを説明するコメントの必要性を回避します。彼らがfilterStringOut()を呼び出すという特別な責任を負うのが嫌いです。

ちなみに、C++は、_||_演算子をオーバーロードしていない限り、trueでの_||_チェーンの評価を停止します。これは 短絡評価 と呼ばれます。しかし、これは簡単なマイクロ最適化であり、関数に副作用がない限り(以下のものなど)、コードを読み取るときに無視できます。

以下は、不必要な詳細をドラッグすることなく、拒否文字列の定義を明確にする必要があります。

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

filterStringOut()を呼び出す必要がなくなったため、要件テスト関数は短くなり、その名前ははるかに単純になります。また、中身を見なくても簡単に理解できるように、依存しているものすべてをパラメータリストに入れました。

_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;
}
_

人間用にrequiredSubstringforbiddenSubstringを追加しました。彼らはあなたを遅くしますか?テストして見つけてください。可読コードを実際に高速にする方が、時期尚早に最適化されたコードを可読または実際に高速にする方が簡単です。

関数が遅くなることがわかった場合は、人間に判読不能なコードを実行する前に インライン関数 を調べてください。繰り返しますが、これにより速度が向上するとは限りません。テスト。

ネストされたforループよりも読みやすいものがあると思います。これらをifと組み合わせると、本当の 矢印アンチパターン が得られ始めました。ここでの教訓は、小さな関数は良いことだと思います。

10
candied_orange

述語にラムダを使用し、次に 標準アルゴリズム のパワーと短絡を使用します。複雑でエキゾチックな制御フローは必要ありません。

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);
    }
}
4
Deduplicator

外側のループ(続行したいループ)のコンテンツをalambdaにするオプションもあります。単にreturnを使用します。
ラムダを知っていれば、驚くほど簡単です。基本的には、ループ内部を_[&]{_で開始し、}()で終了します。内部では、いつでも_return;_を使用して離れることができます。

_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

    }
}
_
1
Aganju

私は@dganeliesが出発点として正しい考えを持っていると思いますが、私はさらに一歩踏み出すことを検討すると思います:(ほぼ)すべてのコンテナー、基準、およびアクションに対して同じパターンを実行できる汎用関数を記述します:

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);
    }
}

次に、filterStringsは基準を定義し、適切なアクションを渡します。

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);
}

もちろん、その基本的な問題に取り組む他の方法もあります。たとえば、標準ライブラリを使用すると、std::remove_ifと同じ一般的な順序で何かが欲しいようです。

1
Jerry Coffin

いくつかの答えは、コードの主要なリファクタリングを示唆しています。これはおそらく悪い方法ではありませんが、質問自体に沿った答えを提供したいと思いました。

ルール#1:最適化前のプロファイル

最適化を試みる前に、常に結果のプロファイルを作成してください。そうしないと、かなりの時間を浪費していることに気付くでしょう。

言われていること...

実際、この種のコードをMSVCで個人的にテストしました。ブール値は進むべき道です。ブール値に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;

MSVC(2008)では、リリースモード(通常のオプティマイザー設定)で、コンパイラーが類似のループを最適化して、gotoバージョンとまったく同じオペコードのセットにした。 booleanの値が制御フローに直接結び付けられており、すべてを排除していることを確認するのに十分スマートでした。私はgccをテストしていませんが、同様のタイプの最適化を実行できると思います。

これには、gotoに勝る利点があり、単一の命令のパフォーマンスを犠牲にすることなく、gotoを有害だと考える純粋主義者が懸念を表明しないだけです。

1
Cort Ammon