気になる点あればバンバン修正申請ください。
追加で紹介したいことや補足したいことなども全然構いません。
はじめに
C++は非常に複雑で難しく、何でもできる言語として発展しています。
古い言語仕様からモダンな言語仕様まですべてを何でも扱うことが出来ます。
それが時に最も好かれる部分でもあり、最も嫌われる部分でもあることは事実です。
C++はより安全で効率的なコードを書けるように機能を提供しますが、
それを全員が理解して使うことは難しいです。
古のプログラマにとっては、今までの慣習から離れなければなりませんし、
生まれたてのプログラマにとっては、最新のC++の前に過去のC++から一通り学ばなければなりません。
C++登場の1983年から、C++98
, C++11
, C++14
, C++17
, C++20
と進化を続けていくたびに、教科書もどんどん分厚くなっていきます。
C++はこのまま過去の古い言語仕様を抱えながら進化を続け複雑化し、地雷原を歩くかのように慎重に"新しいC++"を書いていけるようにならなければいけないのか、
魔法使いか熟練のプログラマになることでしか安全なコードを書くことができないのか、拡大するC++の行き着く果てはどうなっているのでしょうか。
そういった問題点を改善しようとする取り組みが、C++ Core Guidelinesです。
Core Guidelines の発表
2015年Bjarne Stroustrup氏がC++ Core Guidelineを発表しました。
C++ Core Guidelineとは、"ModernC++コードを書くための信頼できるガイドライン" です。
最初の主要な作成者とメンテナはBjarne StroustrupとHerb Sutterであり、これまでのガイドラインはCERN
Microsoft
Morgan Stanley
およびその他のいくつかの組織の専門家からの貢献によって開発されました。
最初の主要な作成者とは、以下の二人の人物のことです。
Bjarne Stroustrup
ビャーネ・ストロヴストルップはデンマークのオーフス生まれの計算機科学者。
オブジェクト指向プログラミングを可能にした言語であるC++を1983年に開発した。
Herb Sutter
ハーブサッターは著名な C++ エキスパート。
C++/CLIのソフトウェア設計のリーダーを務め、C++/CXとC++AMPのリードデザイナーを務めた。
Core Guidelinesを眺めてみる
少しリンクを開いてみると、莫大な量の文字と項目が並んでいることがわかると思います。
すべて英語ですが、
タイトルを日本語訳して見やすくした取り組みなどもあります。
実際にCore Guidelinesの中から一部抜粋して、
どのようなことが書かれているか見てみようと思います。
-
I.11: 生のポインター
T*
または参照T&
によって所有権を譲渡しない- 理由 : 呼出元または呼出先がオブジェクトを所有しているかどうか疑わしい場合は、リークまたは早期破壊が発生する。
- 返されたXを削除するのは誰か?参照が返された場合、問題を特定するのは困難です。
- 値を返すことを検討してください。
- 結果が大きい場合は移動セマンティクスを使用してください。
- 代替案 : スマートポインタを使用して所有権を渡します。
- ただしこれはオブジェクト自体を返すよりも洗練されておらず、多くの場合効率が低い。
- 参照セマンティクスが必要な場合にのみスマートポインターを使用してください。
X* compute(args) // ダメ!
{
X* res = new X{};
// ...
return res;
}
-
I.12: nullであってはならないポインタを
not_null
と宣言する- nullptr エラーの逆参照を回避するのに役立ちます。
- nullptr の冗長なチェックを回避してパフォーマンスを向上させるため。
- 注 :
not_null
はGuidelines Support Libraryで定義されています。
int length(const char* p); // length(nullptr) が有効かどうかは不明です
length(nullptr); // OK?
int length(not_null<const char*> p); // 良い:pをnullptrにすることはできないと想定
int length(const char* p); // pがnullptrになる可能性があると想定する必要があります
-
-
( ptr , size )
スタイルのインターフェイスはエラーが発生しやすくなります。 - 配列へのポインタは、呼び出し先がサイズを決定できるように、何らかの規則に依存する必要があります。
- 例を見てみましょう
void copy_n(const T* p, T* q, int n);
- 配列内の要素が
n
よりも少ない場合は、おそらく無関係なメモリを上書きしてしまう。 - 配列内の要素よりも
q
が少ない場合は、おそらく無関係な記憶をいくつか読みこんでしまう。 - どちらも未定義の動作であり、潜在的に非常に厄介なバグです。
- 配列内の要素が
- 代替案 : 明示的なスパンの使用を検討してください。
void copy(span<const T> r, span<T> r2);
-
このような、
ModernなC++を書くための、最も信頼できるとされるガイドラインが莫大な量並んでいます。
これらのページを編集しているのが、先程のBjarne Stroustrup氏とHerb Sutter氏です。
中には「これは迷信なので守る必要はない」とされる悪い慣習などについても紹介されています。
🐱: じゃあ、これ(ガイドライン) 全部暗記して
🐥: わ、わかんないッピ...
C++ Core Guidelinesのコンセプトは、明確なガイドラインを設けるということだけではありません。
他にもっと重要な役割があります。
Core Guideline のコンセプト
- 最も信頼できる明確なガイドラインであること
- より多くのロジックエラーを検出すること
- ガイドライン違反を検出して警告を出してあげること
- それによって、どんな人でも体系的にModernC++を学べるようにすること
的なことです(勝手にそれっぽくまとめた)。
こちらのページでは、Core Guidelinesが以下のように紹介されています。
1.多くのロジックエラーを検出する
このガイドラインは、インターフェイス、リソース管理、メモリ管理、同時実行性など、比較的高いレベルの問題に焦点を当てています。
このような規則は、アプリケーションのアーキテクチャとライブラリの設計に影響します。
ルールに従うと、静的に型が安全で、リソースリークがなく、比較的新しい一般的なコードよりも、はるかに多くのロジックエラーをキャッチするコードが作成されます。
それでいて、プログラムの実行速度は速いままです。
2.ガイドライン違反に対して警告を出す
ガイドラインルールは、分析ツールでサポートされるように設計されています。
ガイドライン違反が見つかった場合にはそのコードに対する警告と、違反したルールへのリンクが示されます。
コーディングを始める前から、ガイドラインのすべてのルールを覚えておく必要性はありません。
3.他言語から来た人、経験の浅い人のスピードアップを助ける
実際にコード分析を通したとき明白、あるいは些細なルールのいくつかを見つけるでしょう。
ガイドラインの目的の1つは、経験の浅い人や、異なる背景や言語から来た人がスピードアップするのを助けることです。
具体的なガイドラインを提示する流れ
実際にCore Guidelinesのコード分析を有効にすると、以下のようなことが起きます。
🐥: new書いちゃおw
🤖 : newは使わないで、unique_ptr使ってね? [R.11]
🤖 : あとこれ、nullチェックされてないよね? [F.23]
🤖 : ポインタ演算はやめてね? [Bounds.1] [ES.42]
🤖 : 配列操作したいならspan使おうね? [I.13]
🐥: ゎ...ぁ...
🤖 : 泣ぃちゃった...
Core Guidelinesによるコード分析は、その場でプログラマの破ったルールを指摘します。
その時、どのガイドライン項目のどのルールに違反したのか?何故このコードは非推奨なのか?
じゃあ、具体的にどう書くのが正解なのか?ということを説明してくれます。
~ 数年後 ~
🐥: C++ 完全に理解した
🤖: ☺️
この流れを実現して、「何でもできる複雑なC++」の中で
誰でも自然にModernC++を学んで書けるようにコントロールしようとしているのが、
C++ Core Guidelinesの一連の取り組みです。
補足として、検出したガイドライン違反をエラーとせず警告とすることで、古いC++のコードを保ったまま新しいコードを書かせることができるようになっています。
Core Guidelines の構成
-
Core Guidelines
最も信頼できるC++を書くためのルール達のことです。-
Core Guidelines Checker
コードの解析を通してコードのルール違反を見つけます。 -
Core Guideline Support Library
C++ Core Guidelinesに従うことを容易にするための、サポートライブラリです。
not_null<T>
やspan<T>
などがその一例です。
-
Core Guidelines Checker
Core Guidelines Checkerは、C++コードを分析しCore Guidelinesの規約違反を発見すると、警告を出してくれる優れたツールです。
Visual Studioには標準で搭載されており、有効にするだけでコードの分析を実行できます。
の紹介よると、この解析ツールは
モダンな静的解析は lint よりもはるかに優れており、厄介で複雑なバグを早期に発見し、性能を改善する機会を見つけ、一貫性のあるスタイルおよびライブラリとAPIの適切な利用を促す
だそうです。
実際に分析した例を見てみる
Core Guidelines Checkerが有効だと、次のような関数を作った場合
void hoge(int* array, int length) // C26429, C26440
{
for(int i = 0; i < length; ++i)
{
array[i] = 1; // C26481
}
}
int main()
{
int ary[] = { 9,8,7,6,5,4,3,2,1,0 };
hoge(ary, 10); // C26485
// ary == { 1,1,1,1,1,1,1,1,1,1 }
return 0;
}
以下の警告が出るようになります。
このコードには多くの危険が含まれていると警告してくれています。
警告では、それぞれ一目でガイドラインのどの項目に違反したのかがわかります。
Core Guidelinesの(f.6, bounds.1)の項目などを参照すると、具体的な根拠とサンプルを提示して説明を受けることができます。
実際に、先程の関数を以下のように呼び出すと事前にエラーを検出できず、配列の範囲外のメモリを書き換えてしまいます。
int ary[] = { 9,8,7 }; // 要素数 3
hoge(ary, 10); // 要素数 10を指定
Core Guidelinesこうした危険なコードをを分析して警告し、"エラーをしっかりと検出できる"コードに修正させることで危険を防ごうとしています。
Quick Start
このチェックツールはVisual Studioに標準で搭載されており、すぐに有効化してコード分析を実行することができます。
1: Microsoft Code Analysisの有効化
2: 規則セットの構成
C++ Core Checkの選択肢が含まれていると思います。
Microsoftすべての規則を選択するか、個別に選択します。
Code Analysisの手動実行
分析/ Code Analysisの実行/ でコード分析を実行します。
Code Analysisのビルド時実行
ビルドに対するCode Analysis
を有効するとビルド時間が伸びます。
以前はこれが原因なのにも関わらず、なぜビルド時間が長いのか特定することができず非常に苦労したことがあるため、個人的には手動をおすすめします...
Core Guidelines Support Library
Core Guidelines Support Libraryは
Core Guidelines に従うことを容易にするための、サポートライブラリです。
vcpkgでは、ms-gsl
というパッケージ名で公開されています。
具体的に、どのようなことがこのライブラリによってできるのかを紹介したいと思います。
not_null
中身がnullptrではないことを保証します。関数内部などで型にnullが入った状態で動作することがなくなり、関数内部で何段階もnullチェックを行うよりも有用です。
例えば、void func(gsl::not_null<int*> ptr)
に対して func(nullptr)
が入った瞬間にエラーを検出できます。
void hoge(gsl::not_null<int*> ptr) // エラー!
{
// ...
}
void main()
{
int* ptr = nullptr;
hoge(ptr);
}
span
void func(int* ary, int length)
のような不安定なコードをvoid func(gsl::span<int> ary)
と書き換えることなどができます。データにアクセス時に境界チェックを行い、連続したメモリに安全にアクセスできます。spanはc++20でも追加されました。
似たようなことをするのにvoid func(const std::vector<int>& vec)
が使用できます。spanとの違いは、spanが所有権を持たず配列のビューとして機能することなどにあります。
void print_buffer(gsl::span<uint8_t> buffer) // span
{
for(auto i = 0; i< buffer.size(); ++i)
{
printf("%u", buffer[i]);
}
}
int main()
{
std::vector<uint8_t> buffer = { 0,1,2,3,4,5,6,7,8,9 };
print_buffer(buffer); // 0,1,2,3,4,5,6,7,8,9
return 0;
}
spanにはpop_back() などのメソッドがないため、関数内などで追加削除が行われないという保証ができます。
narrow, narrow_cast
narrow
は、数値型が、型変換によって値が損失した際にエラーを吐くことができます。
narrow_cast
は、型変換によって値が失われることを許容します。
例えば、
uint16_t source = 0xffff; // 範囲: 0x00 ~ 0xffff (0 ~ 65535)
uint8_t value = source; // 範囲: 0x00 ~ 0xff (0 ~ 255)
// value == 0xff (255) // 上位8bitの情報が失われる
これを意図した挙動だとする場合、gsl::narrow_cast<T>
として明示すればいいですが、
意図していない場合はgsl::narrow<T>
と記述し、値が欠損してしまった場合にエラーを発生させることができます。
uint16_t source = 0xffff;
uint8_t value = gsl::narrow<uint8_t>(source); // エラー!
Bjarne Stroustrup氏のインタビュー
こちらでCore Guidelinesに対してのBjarne Stroustrup氏の考えを知ることができる質問があったため引用します。
C++の複雑性について
"
Bjarne Stroustrup氏 :
「どんな言語であっても、総合的なルールを挙げていくと何百ページにもなります。完成するまで、ルールとページはもっと増えるでしょう。」
「CやJavaにこうした総合的なものが欲しければ、おそらくもっとたくさんのルールが必要になったでしょう。」
「私たちは安全性だけを懸念しているのではありません。開発者が開発を加速し、優れた性能を実現し、メンテナンスを簡単にし、ライブラリの利用といったテクニックを取り入れる手助けをしたいのです。」
「すべてのプログラマにすべてのルールが必要なわけではありませんし、すべてのルールを読むことを推奨しているわけでもありません。」
「ツールがルールを知っており、プログラマの破ったルールを指摘します。」
「その場で、根拠とサンプルを提示して説明します。」
"
Core Guidelinesの取り組みは、言語仕様を制限した安全で使いやすいC++を作るための土台作りか?
"
Bjarne Stroustrup氏 :
「サブセット言語にするつもりはありません。そういうのは失敗するものです。」
「一つの理由は、古い機能を使ったC++コードは何十億行もあり、それらがすぐになくなることはないためです。」
「もう一つの根本的な理由は、よりハイレベルな/安全な/使いやすい機能を実装するために、危険な/安全でない/ローレベルな/間違いやすい機能が必要になるためです。」
「ハードウェアを直接扱えるC++の能力を損なってはいけません。」
"
おわりに
他のツールでもコード分析はできるらしいです(僕は触ったことない) -> clang-tidy
コード分析を有効にするだけでCore Guidelinesを真に体験することができるので、是非認知されて流行って欲しいと思います。