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


8

これは簡略化されたサンプルです。基本的に、文字列リストから文字列をチェックします。チェックにパスすると、その文字列(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チャットではfor、チェックがパスした場合にtrueを返すブール関数にループを配置することが提案されました。したがって

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

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

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



3
C ++にはラベル付きのブレークがありませんgoto。そのため、評判が悪いにもかかわらず、正規の受け入れられた慣習は、を介してそれらをエミュレートすることです。名前を恐れるな-概念を恐れる。
Kilian Foth、2018年

2
@Akiva:実際にパフォーマンスの違いを測定しましたか?そして、「goto」がネストされたループから抜け出すのに受け入れられる方法であると聞いたからといって、明確で簡潔な名前の関数を導入する代わりの方法が読みにくくなることを意味するものではありません。
Doc Brown

3
@Akiva:これをベンチマークするのは非常に簡単です。特別なツールやスキルは必要ありません。ループ内のいくつかのテストデータでこの関数を呼び出す小さなプログラムを数回(おそらく数百万回)設定し、実行時間を測定しますストップウォッチ。クリーンアップしたコードで同じことを行います。違いはごくわずかです(もちろん、コンパイラの最適化を使用することを忘れないでください)。
Doc Brown

回答:


16

ネストしないでください。代わりに関数に変換してください。そして、それらの関数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;
}

1
「reoveIfImproperLength」タイプミス。:)
Neil

5
3条件のチェック機能が自由副作用まま(すなわち代わりに、単にブール条件を返し、呼び出し元(みましょう「リムーブ場合は」やっていれば良いですParsingTools::filterStrings呼び出す)filterStringOut(i)dagneliesの答えに示すように、機能を。
rwong

したがって、C ++にないbreakステートメントの基礎として、関数呼び出しのセマンティクスを使用しています。非常に賢い。
ライアンライヒ

13

より鳥の視点から見ると、コードを次のようにリファクタリングします...(疑似コードでは、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)


10

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

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

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

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


1
これは他の2つの答えの組み合わせですが、これは多くの価値を追加します。「人間向け」にして、コードを「不明瞭」にする前にパフォーマンスをテストし、簡単にテストできるようにします。素晴らしいもの!
carlossierra

1
実際、私が意図的に使用したので! isProperStringはなく使用しましたisImproperString。私は関数名の否定を避けがちです。後で実際に適切な文字列であるかどうかを確認する必要があると想像してください!isImproperString。二重否定のせいで、どちらが混乱する傾向があるかが必要になります。
dagnelies

@dagneliesよろしいですか?
candied_orange

4

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

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

外側のループ(続行するループ)の内容をラムダにするオプションもあり、単にを使用します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

    }
}

3
(1)実際に電話するすぐにラムダをは、閉じ中括弧の終わりに、呼び出しを行う必要があります(括弧のペアを使用して、引数のリストを使用するかどうかにかかわらず)。(2)を使用しcontinuebreakと交換する必要がある3つの場所すべてreturn。コードは最初の場所(を使用するcontinue)を変更せずに残しているように見えますが、コードもラムダ内にあり、continueステートメントがループであるスコープを見つけられなかったため、それも変更する必要があります。
rwong

赤信号を待っている間にそれを書いた。修正。
アガンジュ

1

私は@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

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

ルール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バージョンとまったく同じopcodeのセットまで、同様のループを最適化しました。booleanの値が制御フローに直接結び付けられており、すべてを排除していることを確認するのに十分スマートでした。私はgccをテストしていませんが、同様のタイプの最適化を実行できると思います。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.