コードを書く段階でエラーの可能性を減らす方法。 注N3

PVS-Studio VS QT

これは、コードをよりシンプルで信頼性の高いものにするのに役立つ新しいプログラミングトリックのペアについてお話ししたい3番目の記事です。 前の2つのメモは[1]と[2]にあります。 今回は、Qtプロジェクトから例を取り上げます。



はじめに



プロジェクトQt 4.7.3。 理由があるので勉強しました。 PVS-Studioのユーザーは、Qtライブラリに基づいて構築されたプロジェクトのチェックに関しては、分析がやや弱いことに気付きました。 これは驚くことではありません。 静的分析を使用すると、コンパイラーよりも高いレベルでコードを見るため、エラーを見つけることができます。 したがって、彼はコードの特定のパターンと、さまざまなライブラリの機能が何をするかを知っている必要があります。 そうでなければ、彼は多くの素晴らしい間違いを通り過ぎます。 例で説明します。

if (strcmp(My_Str_A, My_Str_A) == 0)
      
      





文字列をそれ自体と比較しても意味がありません。 しかし、コンパイラーは沈黙しています。 彼はstrcmp()関数の本質については考えていません。 彼には十分な心配があります。 しかし、静的アナライザーは何かが間違っていると疑うかもしれません。 Qtには独自の種類の文字列比較関数-qstrcmp()があります。 また、それに応じて、アナライザーは次の行に注意を払うようにトレーニングする必要があります。

 if (qstrcmp(My_Str_A, My_Str_A) == 0)
      
      





Qtライブラリをマスターし、特殊なチェックを作成することは、大規模で体系的な作業です。 そして、この作業の始まりは、ライブラリ自体のチェックでした。



警告の表示が終了したので、コードの改善に関するいくつかの新しい考えを熟成しました。これは、皆さんにとって興味深く有用なものになることを願っています。



1.変数が宣言されているのと同じ順序で変数を処理する



Qtライブラリコードは非常に高品質であり、事実上エラーがありません。 しかし、多数の過剰な初期化、過剰な比較、または変数の値の過剰なコピーが明らかになりました。



明確にするために、いくつかの例を示します。

 QWidget *WidgetFactory::createWidget(...) { ... } else if (widgetName == m_strings.m_qDockWidget) { <<<=== w = new QDesignerDockWidget(parentWidget); } else if (widgetName == m_strings.m_qMenuBar) { w = new QDesignerMenuBar(parentWidget); } else if (widgetName == m_strings.m_qMenu) { w = new QDesignerMenu(parentWidget); } else if (widgetName == m_strings.m_spacer) { w = new Spacer(parentWidget); } else if (widgetName == m_strings.m_qDockWidget) { <<<=== w = new QDesignerDockWidget(parentWidget); ... }
      
      





ここでは、同じ比較が2回繰り返されます。 これは間違いではなく、完全に冗長なコードです。 別の同様の例:

 void QXmlStreamReaderPrivate::init() { tos = 0; <<<=== scanDtd = false; token = -1; token_char = 0; isEmptyElement = false; isWhitespace = true; isCDATA = false; standalone = false; tos = 0; <<<=== ... }
      
      





繰り返しますが、間違いではなく、変数の完全に不必要な二重初期化です。 そして、私は多くの同様の重複アクションを数えました。 それらは、比較、割り当て、初期化の長いリストが原因で発生します。 変数がすでに処理されていることは単に見えないため、不必要な操作が表示されます。 このような重複したアクションが発生すると、次の3つの不快な結果を挙げることができます。

  1. 重複すると、コードの長さが長くなります。 また、コードが長いほど、別の複製を簡単に追加できます。
  2. プログラムロジックを変更し、1つのチェックまたは1つの割り当てを削除する場合、このアクションの複製により、数時間の刺激的なデバッグが可能になります。 自分自身を想像して、「tos = 1」と書いて(最初の例を参照)、プログラムの別の部分で、なぜ「tos」がまだゼロなのか疑問に思います。
  3. 速度を落とします。 ほとんど常に、そのような状況では無視されますが、それはまだ存在しています。


私たちのコードに重複する場所がないと確信したことを願っています。 それらに対処する方法? 通常、このような初期化/比較はブロック化されます。 そして、同じ変数ブロックがあります。 変数宣言の順序とそれらを操作する手順が一致するようにコードを記述することは合理的です。 あまり良くないコードの例を次に示します。

 struct T { int x, y, z; float m; int q, w, e, r, t; } A; ... Am = 0.0; Aq = 0; Ax = 0; Ay = 0; Az = 0; Aq = 0; Aw = 0; Ar = 1; Ae = 1; At = 1;
      
      





当然、これは概略的な例です。 ポイントは、初期化がシーケンシャルでない場合、2つの同一行を書く方がはるかに簡単だということです。 上記のコードでは、変数「q」は2回初期化されます。 また、コードを流に見ると、エラーはほとんど見えません。 変数が宣言されているのと同じ順序で変数を初期化すると、そのようなエラーは発生しなくなります。 コードバージョンの改善:

 struct T { int x, y, z; float m; int q, w, e, r, t; } A; ... Ax = 0; Ay = 0; Az = 0; Am = 0.0; Aq = 0; Aw = 0; Ae = 1; Ar = 1; At = 1;
      
      





もちろん、変数を宣言順に記述して作業することは常に可能ではないことを知っています。 しかし、多くの場合、これは可能かつ有用です。 追加の利点は、コード内をナビゲートしやすくなることです。



推奨事項。 新しい変数を追加するときは、他の変数との相対的な位置に応じて、その初期化と処理が確実に実行されるようにしてください。



2.表形式の方法が適しています。



表形式のメソッドは、S。McConnellによってN18章の「Perfect Code」という本でよく書かれています[3]:



テーブルメソッドは、ifやcaseなどの論理式を使用するのではなく、テーブル内の情報を検索できるスキームです。 論理演算子を使用して選択できるほとんどすべてのものは、テーブルを使用して選択できます。 単純な場合、論理式はより単純で明確です。 しかし、論理構造の複雑さにより、テーブルはより魅力的になります。



そのため、プログラマがいまだに巨大なswitch()やif-else構文の密林を好むのは残念です。 自分で克服することは非常に困難です。 まあ、もう1つの「ケース:」または少しの「if」が害を及ぼさないようです。 しかし、彼は害を及ぼします。 そして、経験豊富なプログラマーでさえ、新しい条件を追加できませんでした。 例として、Qtで見つかった欠陥のペア。

 int QCleanlooksStyle::pixelMetric(...) { int ret = -1; switch (metric) { ... case PM_SpinBoxFrameWidth: ret = 3; break; case PM_MenuBarItemSpacing: ret = 6; case PM_MenuBarHMargin: ret = 0; break; ... }
      
      





長期スイッチ()。 そして、もちろん、忘れられたブレークステートメントがあります。 「ret」変数に異なる値が2回連続して割り当てられているため、アナライザーはこのエラーを検出しました。



おそらく、何らかの種類のstd :: map <PixelMetric、int>があり、メトリックと符号付きの数値との間にラベルを明示的に設定した方がはるかに良いでしょう。 関数を実装するための他の表形式のオプションを考え出すことができます。



別の例:

 QStringList ProFileEvaluator::Private::values(...) { ... else if (ver == QSysInfo::WV_NT) ret = QLatin1String("WinNT"); else if (ver == QSysInfo::WV_2000) ret = QLatin1String("Win2000"); else if (ver == QSysInfo::WV_2000) <<<=== 2003 ret = QLatin1String("Win2003"); else if (ver == QSysInfo::WV_XP) ret = QLatin1String("WinXP"); ... }
      
      





コードでは、変数 'ver'を2回定数WV_2000と比較します。 良い例は、場所が表形式のメソッドの場合です。 たとえば、このようなメソッドは次のようになります。

 struct { QSysInfo::WinVersion; m_ver; const char *m_str; } Table_WinVersionToString[] = { { WV_Me, "WinMe" }, { WV_95, "Win95" }, { WV_98, "Win98" }, { WV_NT, "WinNT" }, { WV_2000, "Win2000" }, { WV_2003, "Win2003" }, { WV_XP, "WinXP" }, { WV_VISTA,"WinVista" } }; ret = QLatin1String("Unknown"); for (size_t i = 0; i != count_of(Table_WinVersionToString); ++i) if (Table_WinVersionToString[i].m_ver == ver) ret = QLatin1String(Table_WinVersionToString[i].m_str);
      
      





もちろん、これは単なるプロトタイプですが、表形式のメソッドの概念をよく示しています。 そのようなテーブルのエラーを特定する方がはるかに簡単であることに同意します。



推奨事項。 テーブルメソッドを使用して関数を書くのを怠らないでください。 はい、少し時間がかかりますが、後で報われます。 新しい条件を追加する方が簡単かつ迅速になり、エラーの可能性がはるかに低くなります。



3.その他の興味深い



Qtは大きなライブラリであるため、その高品質にもかかわらず、さまざまなエラーが見つかることがあります。 多数の法則が適用されます。 * .cpp、* .hおよびQtプロジェクトの類似ファイルのサイズは約250メガバイトです。 エラーは起こりそうにないので、大きなコードではそれを実現することはかなり可能です。 Qtで発見した他のエラーに基づいて、いくつかの推奨事項を作成することは困難です。 私が好きないくつかの間違いについて説明します。

 QString decodeMSG(const MSG& msg) { ... int repCount = (lKeyData & 0xffff); // Bit 0-15 int scanCode = (lKeyData & 0xf0000) >> 16; // Bit 16-23 bool contextCode = (lKeyData && 0x20000000); // Bit 29 bool prevState = (lKeyData && 0x40000000); // Bit 30 bool transState = (lKeyData && 0x80000000); // Bit 31 ... }
      
      





&の代わりに&&演算子がランダムに使用されます。 コードにコメントを含めると便利であることに注意してください。 これは本当に間違いであり、ビットが実際に処理される方法であることがすぐに明らかになります。



次の例は、長い式に関するものです。

 static ShiftResult shift(...) { ... qreal l = (orig->x1 - orig->x2)*(orig->x1 - orig->x2) + (orig->y1 - orig->y2)*(orig->y1 - orig->y1) * (orig->x3 - orig->x4)*(orig->x3 - orig->x4) + (orig->y3 - orig->y4)*(orig->y3 - orig->y4); ... }
      
      





間違いありませんか? そうです、移動中は気付かないでしょう。 わかりました、教えます。 問題はここにあります:「orig-> y1-orig-> y1」。 それでも、3番目の乗算は気になりますが、おそらくそうです。



はい、別の質問です。 そして、結局のところ、プログラムにもそのような計算ブロックがありますか? PVS-Studio静的コードアナライザーを試す時が来ましたか? だから、宣伝した。 さらに進みましょう。



初期化されていない変数の使用。 これらは、大規模なアプリケーションで使用できます。

 PassRefPtr<Structure> Structure::getterSetterTransition(Structure* structure) { ... RefPtr<Structure> transition = create( structure->storedPrototype(), structure->typeInfo()); transition->m_propertyStorageCapacity = structure->m_propertyStorageCapacity; transition->m_hasGetterSetterProperties = transition->m_hasGetterSetterProperties; transition->m_hasNonEnumerableProperties = structure->m_hasNonEnumerableProperties; transition->m_specificFunctionThrashCount = structure->m_specificFunctionThrashCount; ... }
      
      





ここでも、長時間目を痛めないようにヒントを与える必要があります。 変数「transition-> m_hasGetterSetterProperties」の初期化を確認する必要があります。



私たちがプログラミングを始めたとき、私たちのほとんど全員がその精神に間違いを犯したと確信しています。

 const char *p = ...; if (p == "12345")
      
      





そして、strcmp()のように、一見奇妙な関数が必要な理由がわかったのです。 残念ながら、C ++言語は非常に過酷であるため、経験のあるプロの開発者であるため、長年後にこのような間違いを犯す可能性があります。

 const TCHAR* getQueryName() const; ... Query* MultiFieldQueryParser::parse(...) { ... if (q && (q->getQueryName() != _T("BooleanQuery") ... ... }
      
      





他に何を表示します。 たとえば、変数値の誤った記述交換。

 bool qt_testCollision(...) { ... t=x1; x1=x2; x2=t; t=y1; x1=y2; y2=t; ... }
      
      





これは、非常に単純なコードでも間違いを犯す例です。 そのため、配列の境界を越えるというトピックに関する例はまだありません。 これで次のようになります。

 bool equals( class1* val1, class2* val2 ) const { ... size_t size = val1->size(); ... while ( --size >= 0 ){ if ( !comp(*itr1,*itr2) ) return false; itr1++; itr2++; } ... }
      
      





サイズ変数には符号なしの型があるため、条件 "--size> = 0"は常にtrueです。 同一のシーケンスを比較すると、配列の境界を超えます。



続けることができます。 プログラマとして、このようなボリュームのプロジェクトのエラーを1つの記事で説明する方法はないことを理解してください。 したがって、最後のスナックの場合:

 STDMETHODIMP QEnumPins::QueryInterface(const IID &iid,void **out) { ... if (S_OK) AddRef(); return hr; }
      
      





「if(hr == S_OK)」または「if(SUCCEEDED(hr))」の精神に何かがあるはずです。 マクロS_OKは0以外の何物でもありません。したがって、ここではリンク数の不正確なカウントによるbyakが避けられません。



結論の代わりに



ご清聴ありがとうございました。 静的コード分析を使用すると、コードのデバッグや保守よりも便利なことに多くの時間を節約できます。



また、あなたや他の誰かのコードで見つけた、診断ルールを実装できるエラーの興味深い例を送ってくれた読者にも感謝します。



書誌リスト



  1. アンドレイ・カルポフ。 コードを書く段階でエラーの可能性を減らす方法。 注N1。 http://habrahabr.ru/blogs/cpp/115143/
  2. アンドレイ・カルポフ。 コードを書く段階でエラーの可能性を減らす方法。 注N2。 http://habrahabr.ru/blogs/cpp/116397/
  3. McConnell S. Perfect Code。 マスタークラス/あたり 英語から -M。:出版および取引会社「ロシア語版」。 サンクトペテルブルク:ピーター、2005 .-- 896 pp。:ill。



All Articles