teppy のすべての投稿

配列の要素数の取得方法

世の中には様々な理由で生配列を直接使う場合があります。

要素数が変わったらarray_sizeの数値も忘れずに変更しなければいけませんが、必ず忘れることでしょう。

配列の要素は静的に決定するので計算で求めることができます。

C言語の時代から人類は500万回以上も配列の要素数取得マクロを再発明してきました。大体、どんなプロジェクトを覗いても配列サイズ取得のマクロを用意してあることが多かったです。

実際、どれほど使われていたかは定かではありませんが……。

全体のサイズを1要素のサイズで割ることで配列の要素数を計算できます。

これを使えば要素数が増えた時でも自動的に値が更新されます。

ただマクロでは名前空間の指定ができません。

C++的にはテンプレートで書くのがいいでしょう。

関数なので任意の名前空間に閉じ込められます。

ちょっと応用すれば2次元配列の要素も簡単に一発で取得できます。

ただし配列を引数で受け取った先で取得したい場合など、コンパイラが要素数を把握できない状況では これらの取得方法は使えません。

結局、生配列を取り回し良く使うには、先頭アドレスと要素数とのペアとして管理する必要が出てきます。

C++の恩恵を最大限に活用する為にもコンテナクラスを使いましょう。

まとめ

  1. 生配列の要素数を取得する方法を知ろう
  2. 生配列の要素数が取得できないシチュエーションを知ろう
  3. 教義を曲げてでもコンテナクラスを使おう

 

クラスの不定なメモリレイアウト

C++を書きはじめて半年もすると、ほとんどのプログラマはクラスの定義を見ただけで生成されるオブジェクトのメモリレイアウトが浮かんでくるようになります。

構造体AAAのメモリレイアウト

構造体BBBのメモリレイアウト

このように一目です。(ただし浮かんでくるだけで合っているとは言っていない)

 

基本的にクラス内のメンバ変数は定義された順にメモリ上に並びます。

単一継承の場合、親クラス→子クラスというメモリレイアウトになります。

 

もう少し複雑な設計になるとどうでしょうか。

多少複雑になりました。

構造体DDDは仮想関数を持ち、多重継承しています。

これは64bit環境における一例です。

しかし、元のコードだけでは圧倒的に情報が不足しているため、この構造体定義から導き出されるメモリレイアウトは無数にあります。ある意味、どんなレイアウトを想像しても正解と言えなくもありません。

intは4byteかもしれませんし、8byteかもしれません。

vtableのポインタは多くの場合メンバの最初に置かれますがこれはコンパイラ依存です。さらに、そもそも動的なディスパッチを仮想関数テーブルによって実装しない場合はvtableはありません。

同様に多重継承した場合のレイアウトも宣言順とは限らず不定です。

 

クラスのメモリレイアウトに関して少しマイナーな情報もあります。

最もシンプルな例、構造体AAAに戻って話を進めましょう。

このメンバa〜e のメモリレイアウトは不定です。宣言順にabcdeと順序よく配置される保証はありません。

C++の規格ではあるアクセス指定子の範囲において宣言された順にレイアウトされることは保証していますがアクセスレベルを超えた宣言についてのメモリレイアウトには規程がありません。

Nonstatic data members of a (non-union) class with the same access control (Clause 11) are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified (11).

__n3337

この場合d,eが連続したメモリに配置されることのみ保証されます。

aもbもprivateメンバですが、アクセス指定子を超えた場所で宣言されているのでレイアウトの保証対象外です。

もしあなたが複数のプラットフォーム向けにビルドする予定のアプリケーションを書く可能性があるのなら、どのように書くとメモリレイアウトが「不定」になるかを知ることは非常に重要です。

まとめ

  1. メンバ変数はあるアクセス指定子の中では宣言された順にレイアウトされる
  2. アクセス指定子をまたぐ場合のレイアウトは不定
  3. 仮想関数を持ったクラスのレイアウトも実装依存なので不定
  4. 多重継承のレイアウトも実装依存なので不定
  5. メモリレイアウトが不定になるシチュエーションを知ろう

前置インクリメント vs 後置インクリメント

このアーティクルでは前置と後置、2種類のインクリメントについて考察と現状をまとめるものです。

 

長年の常識

従来(~2015)は前置インクリメント(++value)と後置インクリメント(value++)の挙動の違いを正しく完全に理解した上で前置インクリメントを使うべきだ。というのがC++プログラマの基本姿勢でした。

各インクリメントの標準的な実装を以下に示します。

 

後置インクリメントにはひと目で遅くなりそうな処理が見て取れますね。

前置インクリメントがインクリメント処理後、単純に自身の参照を返すのに対し、後置インクリメントではインクリメント前に一時オブジェクトの生成、そしてインクリメント後にはその前に生成した一時オブジェクトを値で返しています。

前置と後置では、単純にオブジェクトをコピーして返す分、普通に考えたら後置の方が遅いよね。というのが従来の認識でした。

「C++ Coding Standards -101のルール、ガイドライン、ベストプラクティス」の中でも、特に後置インクリメントの必然性が無い時は迷わず前置インクリメントを使うことが推奨されてきました。

元の値を必要としないときは前置形式の演算子を使おう

__C++ Coding Standards (p50)

 

新たな主張

「ゲームエンジン・アーキテクチャ第二版」の中の一節を紹介します。

しかし、値が使われる場合、CPUのパイプラインでストールを生じさせないので、ポストインクリメントの方が優秀である。したがって、プレインクリメントの動作が絶対に必要である場合を除いて、必ずポストインクリメントを使う習慣を身につけたほうがよい。

__ゲームエンジンアーキテクチャ第二版 プレインクリメント vs ポストインクリメント

従来の主張と真逆の主張が展開されたのです。後置インクリメントの方が優秀なので出来る限りそちらを使うべきとされました。

ゲームエンジン・アーキテクチャの著者は、UnchartedやThe Last of Usの開発元であるノーティドッグ社のエンジニアです。その彼が後置インクリメントを使うべきと主張したことで大きな話題となりました。

後置インクリメントが優秀とされる理由は以下のとおりです。

  • 前置インクリメントは値を書き換えて戻すのでインクリメント処理が終了するまで戻す値が決まらない
  • それはデータ依存性を生み、深いパイプラインのCPUではストールを発生させる
  • 後置インクリメントはインクリメント処理と戻す値は別インスタンスなので並列に処理が可能
  • よってストールの発生が無い分、後置インクリメントが優秀

なるほど、といった感じです。

 

 考察を深めよう

今後は後置インクリメントに統一だ!今まで書いた前置インクリメントを全部置換しなくちゃ!と、急ぐ前に、本当にゲームエンジン・アーキテクチャの主張が正しいかは一考の余地があります。

深いパイプラインにおけるストールの可能性はデータ依存性があるかぎり発生しうる問題です。

この部分の主張は正しいです。

しかし、ゲームエンジン・アーキテクチャでは後置インクリメントに発生していたコピーコストについて言及がありません。

文脈を読み解く限り、「コピーコスト<ストールコスト」という前提があるようです。このインクリメントの項以外でもゲームエンジン・アーキテクチャではストールに対する嫌悪を書いてある箇所があり、著者はかなりストールコストに対してナイーブになっているように感じます。

実際にコンソールゲーム機のアーキテクチャにはストールのコストが引くほど高いものもあり、それこそPS2の時代はとにかくCPUの稼働効率を高めるようなコードを意識して書かないと描画ポリゴンが倍は違うというような状況でした。XBOX360の頃でさえ、シーングラフ(ゲームシーンの描画を効率的に管理したりする構造)を作っても、その走査や判定のコストの方が高い為、単純にシーンにある全オブジェクトを順番に描画するだけの方が速かったという経験があります。

 

 一時オブジェクト生成のコスト考察

一時オブジェクトの生成に伴うコストについて、好意的に解釈してみましょう。

例で示した実装の場合、後置インクリメントで変更前の値を返す際にRVOの最適化が期待されるので、前置インクリメントも後置インクリメントも(このインクリメントの結果として使う時の)オブジェクトの生成は1度きりになる可能性があります。厳密には直前のテンポラリオブジェクトが(oldという)名前付きオブジェクトなので、この最適化を期待するためにはコンパイラがNRVO(Named Return Value Optimization)に対応している必要があります。それでもほとんどのコンパイラはNRVOが効くのでコピーコストは低くなる可能性があります。

ちゃんとNRVOが効くコンパイラ(今回はgcc)を使って実際のアセンブラコードを見てみましょう。

ユーザー定義の前置インクリメントと後置インクリメントを用意します。

前置インクリメント、後置インクリメントそれぞれの処理を呼び出す処理を書きます。

pre.cpp

post.cpp

これら2つの処理のアセンブラコードを出力します。

アセンブラコードを読むのが嫌いな人は行数だけ見ればいいです。

 

やっぱり後置インクリメントの方が遅いですね。

では次はコンパイラの最適化オプションをバッチリ有効にしたバージョンで見てみましょう。

 

 

なんということでしょう。

前置も後置もまったく同じ結果になりました。流石、昨今のコンパイラは優秀です。

これは例として示したコードのようにインクリメントの実装詳細まで一つのコンパイル単位に含まれている関係で、コンパイラが副作用がないことを把握しながらインライン展開まで行える為、とにかく結果だけ合うように最速のパフォーマンスを組み上げられる為です。

もう一歩進んで、Hoge::operator++()とHoge::operator++(int)を別のcppに移しました。

それではアセンブラコードを見てみましょう。

まずはhoge.cpp自体のアセンブラコード。

pre.cpp

post.cpp

やはりコード次第では、最適化オプションを有効にしていても、前置インクリメントにアドバンテージがあるケースがありますね。

hoge.cppなんてコンパイラにとったら何のヒントも無いコンパイル単位なので、素直に処理するしかないので当たり前といえば当たり前です。

しかし、世の中には、リンク時最適化というものがあり、オブジェクトコード生成後に再度ファイルを走査し、リンクのタイミングでインライン展開されるような場合もあるので、この例で一概に前置が有利と結論付けることはできません。

ストールコストの考察

ストールが発生するシチュエーションを考えます。

ストールは本来、このインクリメントに限らず、データ依存性を持つあらゆるシチュエーションで発生する恐れがあります。条件分岐、値の更新など、結果を待たなければ先の処理が行えないケースが発生するたびストールは起こりえます。分岐予測など、ストールが実質的に無くなるような工夫ももちろん行われているのですが、予測が外れた場合には、事前準備の甲斐なく、やっぱりストールします。

まず、インクリメントの外、インクリメント結果を使う瞬間に発生するストールについてですが、ただ単に一行

と書く場合、前置と後置で何も変わりません。

次の行以降で使う値はインクリメント後の値なので、前置後置関係無く、値の更新が完了しないと何も始まらないからです。

この例の場合、while文のtest exprに入った段階でiの内容が確定している為、インクリメントの結果を待たずに次の処理を進めることができます。こういうようなシチュエーションでは前置と後置でストールによるコストが変わってきます。

インクリメントの中で発生するストールもについても意識する必要がります。

前述のとおり、ストールはどこでも発生する可能性があります。

このストールがインクリメント演算の中で発生した場合、インクリメント演算の度にCPUはストールしてしまいます。しかし、後置インクリメントであれば、インクリメント演算の結果を待たず次の処理へ移れる為、有利と言えるでしょう。

最悪の場合、インクリメントの中でストールするわ、インクリメントの外でストールするわと、春先の有楽町と同じくらいストールで溢れかえってしまいます。実際にCPUがストールしまくる状態のことを「海外セレブ効果(Celebrity Fashion)」と呼びます。嘘です。

今回のゲームエンジン・アーキテクチャの主張もまさに、後置インクリメントはインクリメントの結果を待たなくても、戻り値のオブジェクトは決定しているのでそのオブジェクトに対する処理はインクリメントの動作と並行して行えるよね。ならストールしないよね。というものです。

逆に、コードの流れの中で、インクリメントした後の結果しか使わないのであれば、前置でも後置でもCPUのコストは変わりません。仮にインクリメント演算子の中で70,000サイクルの処理がかかるとしても、インクリメント後の値しか使わないのであれば、前置でも後置でも等しくその負荷が計上されることになります。

 

計測サンプル

ダラダラとアセンブラコード貼ってる暇があったら実測値をだしてみろよ!投資家は数字でしか動かねーんだよ!というYコンビネーターマインドの方もいるかもしれません。

以下のサイトではインクリメントのコストを複数のコンパイラで比較検証されていますので一例として参考にして頂ければと思います。

インクリメントの前後置速度比較(vc++/gcc/clang)|他人の空似

典型的なfor文を前置後置それぞれのインクリメントを使用する形で作成。
インクリメント対象をunsigned int/iterator/適当に重めのクラスの三種それぞれを対象。
また、最適化により返り値計算が消えることを考えforの真偽判定でインクリメントの返り値を使用するものも用意。
以上をそれぞれ100,000,000回実行をさらに10回繰り返しかかった時間の平均をとる。
(中略)

vc++ gcc clang
int前置インクリメント 46ms 61ms 41ms
int後置インクリメント 51ms 44ms 53ms
クラス前置インクリメント 320ms 47ms 316ms
クラス後置インクリメント 2206ms 388ms 446ms

__インクリメントの前後置速度比較(vc++/gcc/clang)|他人の空似

全計測の結果は元サイトよりご確認下さい。

なお、余談ではございますが闇夜のC++のサイト運営に2億くらい出してもいいよというエンジェル投資家の方がいらっしゃいましたら、ご連絡下さい。

 

現実的な観点

我々は現実の世界で実際にコードを書くプログラマです。そこには机上の論理以外の様々な要素を考慮する必要があります。

結論を出す前に我々が向き合わなければいけない現実的な考慮事項は4つあります。

まず第1に、一時オブジェクトの検証結果ですが、インライン展開が効かない、前置インクリメントにアドバンテージのあるシチュエーションにするには、リンク時最適化の効かないコンパイラ上で、ユーザー定義のインクリメントオペレータ演算子をcpp側に書く必要があります。
インクリメント演算子もデクリメント演算子もクラス書くたびに必ず用意してます(しかもヘッダーファイルじゃなくcpp側に!)という人は別として、一般的に最も多く呼び出されるインクリメント演算子は組み込みのint、もしくはSTLのイテレータに対してではないでしょうか。
その場合、やはり先程の検証でもあったように、コンパイラの素晴らしい最適化により、前置と後置のコストの違いは限りなく少なくなります。

 

第2に、インクリメント演算によって発生するなんらかのストールを回避するにはインクリメント前の値を使うようなコードを心がける必要があります。
例えば、インクリメント演算子を式中で使ったり、インクリメント後もインクリメント前の値に対する処理を書くという感じです。
しかし可読性の観点からすると、あまり推奨されるようなコードではありません。

 

第3に、ストールのコストは肌で感じづらいという問題があります。
処理に直接関係するコストは、例えばアセンブラコードの出力を見れば把握できます。
余分なオブジェクトが生成されているな、とか、最適化されているな、とか。
しかし、あるコードを書いた時にその部分の処理によってCPUにストールが発生しているかというのは極めて把握が難しい問題です。

 

第4に、しかし、それでも、いかんともしがたく、アーキテクチャに引っ張られることがあるという点です。
中盤でも触れましたが、一部のアーキテクチャにおいてはストールの発生を抑えなければロクに処理が動かないというケースがあるのも事実です。そういうアーキテクチャと向き合わなければならない場合、全く一切選択の余地なく、可読性うんぬん以前に、方針を決めざるをえない場合もあります。

 

CPUのアーキテクチャへの理解と、コンパイラの挙動、そして現実にコードを書く我々、この3つを勘案した上で、実際に前置インクリメントを使うべきか、後置インクリメントを使うべきか、また今の時代そういうことは気にしないべきか、考える必要があります。

私は、後置インクリメントを使っている事を指摘した際に「だってゲームエンジン・アーキテクチャに書いてあったからなんとなく」という答えをするプログラマが増えない事だけを願っています。

 

まとめ

  • 前置インクリメントを使うべきという主張の理由を理解しよう
  • 後置インクリメントを使うべきという主張の意図を汲もう
  • 時代の変化とともに柔軟に再考しよう

 

 

レガシー環境でnullptr

nullptrはc++11から追加された最も有益な機能の一つです。

今、このアーティクルを書いている時代は2015年ですが、未だにnullptrの使えない環境が存在します。信じられないでしょうが、例えばコンソールゲームの開発環境では未だにc++03相当の機能しかないコンパイラを相手にしなければいけないことがあります。

このアーティクルはプログラマとしての尊厳を奪われている一部の人向けに存在しているのであり、LLVMを使えるような、いわゆる普通の環境にいる人には全く意味の無いものであることを最初に宣言させて下さい。

nullptrが存在しなかったころnullを表すものは単に数値の0でした。

それは全く不十分なもので、単に0が書かれていてもコンパイラはポインタなのか数値なのかを把握できません。

CODE COMPLETEの一節にこんな言葉がります。

言語の中でプログラムするんじゃなく言語の中へプログラムしよう

__CODE COMPLETE 第二版

つまり言語が用意してくれていない機能や有効な思想は、言語に無いからといって諦めるのではなく、なんとか実現できる方法がないか考えようということです。

ではレガシーな環境でもnullptrが使えるようにオリジナルで実装しましょう。

 

これで数値に代入できない型の完成です。

const 無名クラスで直接nullptrを定義してます。環境によっては名前をつけて別途インスタンスを生成しないとコンパイルエラーになるのでその場合はもうちょっとコードを書く必要があります。

 

ちなみにEffective C++ の第二版にこのnullptrの実装が書いてありましたが、第三版ではバッサリカットされています。

 

まとめ

  1. c++11より昔のコンパイラしか使えない環境ではnullptrは自作しよう
  2. もしくは戻り値にautoを書けるような最新の型推論を備えたコンパイラが使える環境に転職しよう

Return Value Optimization

オブジェクトの生成と破棄には莫大なコストがかかることがあります。

CPUによるメモリ割り当て、コンストラクタ・デストラクタの挙動等、オブジェクトが一つ生成され、破棄されるまでのコストは無視できません。あなたの目の届かないところで提供されたサードパーティ製のライブラリのクラスでは、コンストラクタ内で自分のメンバからCRC値を生成して勝手にツイートする実装になっているかもしれません。

コンパイラも、そんな我々の気持ちを汲んで、なるべく一時オブジェクトの生成を抑制=最適化しようとしてくれます。

そんな最適化の一つが今回説明するRVO(Return Value Optimization)です。

関数の戻り値を値で返す時に発生する一時オブジェクトをコンパイラは可能な限り消し去ります。

 

一切なんの最適化も行われないと仮定すると上記のコードは3回Hogeオブジェクトを生成することになります。

 

しかし、実際には1度しかコンストラクタ・デストラクタは呼ばれません。

RVOが効く為です。

現代の主要なコンパイラにとってこの最適化は当たり前となっており、gccでもclangでも、armccでもRVOは効きます。

Wandboxで際に実行してみると、コンストラクタ・デストラクタがそれぞれ一度しか呼ばれないことが確認できます。

RVOによってコンパイラに生成されるコードの詳細についてはEfficient C++の第四章「戻り値の最適化」にも説明があります。

Named Constructor Idiom

この最適化を念頭に入れておけば、値を返す関数を可読性の高いコンストラクタ呼び出しとして使うことができます。

 

これを Named Constructor Idiomと言います。

More C++ Idiomsにも載っていますので参照すると良いでしょう。

 

まとめ

  1. コンパイラは可能な限り一時オブジェクトの生成を抑制する
  2. 戻り値の最適化によって一時オブジェクトの生成が抑制されることをRVOという
  3. RVOを利用して余計なコストを気にせず可読性を上げる手法をNamed Constructor Idiomという

メソッド内でのthis最適化

順調に動いていたはずのプログラムが突然ダンプを吐いてクラッシュする・・・。

まさに悲劇としか言えません。

ある調査機関によると成人男性の約89%がこの不幸に出会っているという報告もあります。[要出典]

最も多く、そして最も簡単に発生する原因としてnullポインタアクセスが挙げられます。以下のコードを見てみましょう。

このプログラムは実行するとすぐさまクラッシュします。当然ですね。

問題はどこでアクセスエラーが発生するか?です。

正解は5行目の

の部分なのですが、なぜメソッド呼び出し

ではなく、その内部の処理まで進むのでしょうか。

理由はc++のアーキテクチャにあります。

あるクラスのメソッド、あるインスタンスのメソッド呼び出しとは、内部的には通常の関数にインスタンスのポインタを渡しているだけなのです。暗黙の引数としてthisが渡されるイメージです。

ですので、先ほど提示したコードは、C言語的な擬似コードで書くと

というコードに近いイメージになります。

これならアクセスエラーが発生する場所が

だという事がよりイメージしやすいのではないかと思います。

C++でコーディングしていると、ポインタを操作する前にnullチェックをするコードを書くケースが多いのですが、上記を踏まえ、こんなことを考えるかもしれません。

 

そうだ!メソッドの中でチェックしよう!

 

 

と書くかわりに

こんな感じでメソッドの中でチェックすれば、呼び出し側が、色々な場所で呼び出すかわりにnullチェックしなくて済む。と思うかもしれません。

 

 

しかし、このコードは再びアクセスエラーになる可能性があります。

なぜか?

一見、ちゃんとnullチェックもしているように見えます。

 

C++の規格ではメソッド中でthisがnullではないことが保証されているため優秀なコンパイラは単にメソッド内でのthisへのnull判定コードを最適化で消し去ります。

これが今回のアーティクルのタイトルにもなっているメソッド内でのthisの最適化です。

コンパイラオプションによってこの最適化を抑制することもできるのですが、規格に準じたコードではないので書くべきではありません。

 

コラム

行儀よく普通にコーディングしてるとメソッドの中でthisを判定したいことは少ないと思います。こんな最適化に関する知識、それほど重要じゃないと思われるかもしれません。

あるゲームエンジンを書いていた時のことです。

そのゲームエンジンはオブジェクトを生成すると同時に色々な準備を同時に行う必要があったのでファクトリによって生成されました。生成と破棄の詳細についてユーザは知る必要がないのでインスタンスを破棄する時、ユーザーは単に破棄用のメソッドを呼びます。

不特定多数のゲームプログラマが使うようなシステムを実装する場合、ゲームエンジンを書いているプログラマは「とにかく簡単に完結に」 という心理になります。

delete演算子にnullポインタを渡しても支障が無いので、この破棄処理もobjがnullかどうか破棄の前に判定させることを強要しない作りにしたほうがシンプルで良いと考えるのです。

そして……メソッドの中でthisによる判定を書いてしまうことが……あるのです……。

あなたがもし、ゲームエンジンの実装を任されているエンジニアで、なおかつdeleteがnullポインタを許容する事実を知っている場合は、この最適化の事を覚えておいて損はないでしょう。

そして、もしよりシンプルでブレの無いエンジンを目指すなら、そもそもオブジェクトの生成はスマートポインタで返すべきかもしれません。(ハードスペックと使っているコンパイラが許すなら)

 

 

まとめ

  1. メソッド中ではthisがnullじゃないことは規格で保証されている
  2. 故にメソッド中でのthisそのものに対する判定は最適化される場合がある
  3. コンパイラオプションで抑制できるとしてもメソッド内でthis判定するのは規格外なので良くない

 

真に安全なsafe_delete

ポインタをdeleteした後は、対象のポインタの値をnullにした方が安全で良いプラクティスだとされています。

毎回deleteする度にクリア処理を書くのは手間です。

マクロ化してみましょう。

一昔前に良く見かけたマクロです。

マクロはプリプロセスで展開されます。単に展開され、コンパイルエラーもマクロの中とは別の場所を示します。そして何より名前空間を指定することができません。取り回しが良いとは言いづらいですね。

Effective C++でも、かなり最初の方に#defineを使うことへの注意が書いてあります。

言い換えれば「プリプロセッサよりコンパイラを使おう」ということです。#defineは、一般には、言語の一部としては扱われません。この事実から問題が起こることもあるのです。

__Effective C++ 第3版

従いましょう。

引数はTのポインタの参照です。

Tのポインタの参照として受けることでnullptrの代入が呼び出し元に反映されます。間違ってT*だけにしてしまうとこの関数の外に戻った時にポインタの値が元のアドレスのままになってしまうので気をつけて下さい。

 

ここで一旦、safe_deleteのことは忘れて、以下の処理の流れを考えてみます。

前方参照されただけのHogeという型のポインタのインスタンスを生成して、deleteに渡すコードです。

この場合、コンパイラはHogeという型の詳細を知りません。

別のファイルでHogeの中身が定義してあるとしても、このソースのコンパイル時にその情報を持っていません。しかしHogeは前方参照されており、このコード中はポインタでしか扱っていない為、型サイズやメンバ変数の内容を知らなくてもコンパイルできてしまいます。

コンパイラによっては警告を出しますが、シンタックスエラーという訳でもないのでコンパイルは通ります。

Wandbox上でもコンパイルが通ることが確認できます。

なぜ不完全型がコンパイルエラーじゃないかというと、実のところ、不完全型をdeleteに渡すことは規格で許可されているからなのですが、但し書きが付きます。

独自のdelete演算子を定義していたり、トライバルなデストラクタを持たないクラスの場合、規格の保証外となり、未定義動作になります。

未定義動作は、単に不具合の元となりますので絶対にやってはいけません。

絶対にやってはいけないことは、絶対にできないように書く必要があります。

先ほどのsafe_deleteに不完全型をコンパイルエラーにする仕組みを導入しましょう。ランタイムではなくコンパイル時にエラーにするのが肝要です。

不完全型が渡された時に配列の要素が負数になり、そんなものはコンパイラが許さないのでエラーになります。

みんな大好きなboost::checked_deleteも同様のコンセプトの元、用意されています。

これで無事、完全なsafe_deleteを実装することができました。

簡単ですね!

実装する際は配列バージョンも用意するのをお忘れなく!

その先へ

安全なsafe_deleteの実装は完了しました。

ただこれはsafe_deleteの実装が安全なのであって、safe_deleteを使う環境が安全になったわけではありません。同僚プログラマがsafe_deleteを呼び忘れるリスク等を考慮するとスマートポインタなどの使用も検討しましょう。

 

まとめ

  1.  マクロによるSAFE_DELETEは不十分
  2. 不完全型のエラー対応をしよう
  3. 実装する際は配列版も忘れずに

deleteの呼び出しと実装

以下のようなマクロ定義を見かけることがあります。

昔々、マイクロソフトがDirectXのサンプルで上記のようなマクロを定義していたことがあり、恐らく、多くの人がそのサンプルのままで覚えた結果、特に深く考えずに実装しているケースも少なくないのでしょう。

ポインタがnullじゃないことを判定した後、deleteするという処理です。

delete対象のポインタがnullだった場合、どうなるのでしょう?

未定義の動作でしょうか?

アクセス例外でハングアップしたり、ニュージャージー州に住む叔母に勝手にメールを送るような動作を引き起こしたりするでしょうか?

 

答えは「何も起こらない」

 

deleteにnullポインタを渡しても安全であると規格にも明記されています。

In the first alternative (delete object), the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject representing a base class of such an object.

__n3337 5.3.5 Delete

知っている人には当たり前でも、案外知らずにC++を使っている人も多いように感じます。

nullかどうかを気にせずポインタをdelete出来るかわりに、今度は別の、気をつけなければいけないことが生じます。

そう、自分でdelete演算子を定義する場合です。

規格に準ずる為にはnullを許容する実装になっていなければいけません。

まとめ

  1. nullポインタをdeleteに渡しても問題無い
  2. オリジナルのdelete演算子を定義する場合はnullポインタに対応しなければいけない