February 20, 2024
MISRA C++:2023®で、範囲forループのバグを回避しよう
*本記事は Perforce Software社の以下のブログ記事(2024年4月10日時点)の参考訳です。
Avoiding Bugs in Range-Based For-Loops with MISRA C++:2023®
*ブログの内容は更新されている可能性があります。
MISRA C++規格の新版MISRA C++:2023®がリリースされました。前バージョンとの違いなど、この新しい規格への理解をさらに深めるために、Perforce Software社のPrincipal Technical Support EngineerであるDr. Frank van den BeukenによるMISRA C++:2023特集ブログシリーズの第3弾をお届けします。
最初の2つのブログ記事では、新しいMISRAC++規格の概要とC++プログラミング言語の歴史について書きましたが、今回は、forループに関するルールについて少し詳しく見ていきたいと思います
ブログシリーズ第1弾「MISRA C++:2023®について知っておくべきこと」はこちら
ブログシリーズ第2弾「C++プログラミング言語の略史」はこちら
目次
MISRA C++:2023 Rule 9.5.2とは?なぜ重要なのか?
MISRA C++:2023では、Rule 9.5.2というルールが新たに追加されました。これは、範囲for初期化子(for-range initializer)に関数呼び出しを2つ以上含んではならないというルールで、範囲for文で使用する初期化子が一時オブジェクトを生成した際に未定義の動作が起こらないようにするためのものです。
なぜ未定義の動作が起こりえるのかを理解するために、まずはC++の範囲forループについて少し説明しましょう。
C++言語の範囲forループ(Rang-Based For-Loop)とは?
プログラミングでは、コードのブロックを繰り返し実行するためにループが使用されます。繰り返し実行したい回数が決まっている場合には、forループを使用しましょう。
C++の範囲forループは、コンテナに対する反復処理を簡単に表記するために、C++11で追加された機能です。
従来のforループはC言語から派生したもので、ループの初期化(任意)に続き、ループの条件、ループの増分式という構造になっていました。
つまり、コンテナに対する反復処理をforループを使うことで、以下のように記述することができます。
std::vector v = { "Example", "vector", "of", "strings" };
for ( auto &&i = v.begin(); i != v.end(); ++i ) {
std::cout << *i << “ “;
}
std::cout << std::endl;
範囲forを使用すると、イテレータの使用が暗黙的に行われます。
for ( auto &&s: v ) {
std::cout << s << “ “;
}
この方法であれば、同じループがずっと簡潔に記述できます。C++言語規格によれば、省略しないで記述すると以下の通りになります。
{
auto && __range = v;
auto __begin = __range;
auto __end = v.end();
for (; __begin != __end; ++__begin) {
auto &&s = *__begin;
std::cout << s << “ “;
}
}
しかし、この書き方にはひとつ問題があります。上の例では、__range がシンプルな変数 v で初期化されていますが、2つ以上の一時オブジェクトが生成されるような複雑な式が使われるケースもあります。
以下のような2つのループを持ち、文字列の配列を返す関数を例に見てみましょう。
- 上で記載された通りに、スペースで区切られた文字列を出力するループ
- スペースで区切られた文字列の最初の文字を表示するループ
std::vector createStrings() {
return { "Example”, "vector", "of", "strings" };
}
int main() {
for ( auto w: createStrings() ) { std::cout << w << " "; }
std::cout << std::endl;
for ( auto c: createStrings()[0] ) { std::cout << c << " "; }
std::cout << std::endl;
}
このコードを実行すると、最初のループは期待通りに動作しますが、2つ目のループが未定義の動作を引き起こしてしまいます。これは、createStrings()[0]が2つの関数呼び出しを持っているためです。最内部の呼び出しは、createStringsに対してのもので、最外部の呼び出しは、添字演算子[]に対するものです。
‘createStrings’が返す一時オブジェクトが、‘演算子[]’呼び出しの引数として使用されていますが、C++のルールでは、この一時オブジェクトは関数呼び出しの終了時に破棄されます。このため、未定義の動作が発生してしまうことになります。
MISRA C++:2023 Rule 9.5.2で、どのように未定義の動作を防止するのか?
MISRA C++:2023 Rule 9.5.2は、このような未定義の動作を回避するために追加されたもので、範囲for初期化子に含む関数呼び出しはひとつまでというルールになっています。
この問題を避ける方法として、範囲forループの前に、別の宣言で内部の関数呼び出しを行うアプローチも提案されています。例えば、以下の通りです。
auto strings = createStrings();
for ( auto c: strings[0] ) { std::cout << c << " "; }
このようにすれば、初期化子に含まれる関数呼び出しはひとつだけになり、一時オブジェクトの寿命も期待した通りに延長されるため、完全に定義された動作になります。
※ C++23では、初期化子内で作成されるすべての一時オブジェクトの寿命が、for文全体に延長されるようになっているため、この問題の発生を心配する必要はなくなっています。
Helix QACでMISRA C++:2023 ルールの徹底を!
Perforce Software社の「Helix QAC」は、MISRA CおよびMISRA C++のコンプライアンスチェックをはじめ、様々な解析機能を備えた静的解析ツールです。
MISRA C++:2023対応のコンプライアンスモジュールと合わせて使用することで、MISRA C++:2023ルールに対するテストカバレッジ100%を実現することができます。Helix QACは、C/C++言語におけるMISRAルールおよびディレクティブの違反を検出・報告し、コンプライアンス対応を強力にサポートします。
MISRA C++:2023コンプライアンスモジュール is Available Now!!
MISRA C++:2023コンプライアンスモジュールは既に日本でも提供を開始しています。ご興味のある方は、製品ページをご覧いただくか、以下までお問合せください。