カテゴリー別アーカイブ: Language

ADL Firewall

先人たちが書いたコードからは学びが多いものです。

みんな大好きboostのnoncopyableのソースコードを読んでみましょう。

 

8行目のコメントが興味深いですね。

protection from unintended ADL(思いがけないADLの防止)

どういう意味でしょうか。

ご存知のとおり、ADLでは予期せぬネームスペースまで検索の対象になることがあり、時として不幸な不具合の温床になっていまうことがあります。

それというのも、ADLによって、検索されるネームスペースに実引数の名前空間が暗黙に含まれる為なのですが、このboostのコードではその予防策が講じられているというわけです。

21行目で

とあります。

この一行のtypedefにより、本来boost::noncpyable_::noncopyableというアクセスをboost::noncopyableと書くことができるようになります。

しかし、実際のnoncopyableはnoncopyable_というネームスペースに存在している為、ADLによってboostネームスペース全てが検索候補になってしまうことを防いでいるのです。

これがADL Firewallという手法です。

このようにネストしたネームスペースに型を閉じ込めて、ネームスペースごとエイリアスを付けることで、アクセスしやすくするのですが、inline namespace ではADL対策になりませんので気をつけて下さい。

inline namespace はADL対策にはなりません!

大事なことなので2回言いました。

余談

このADL Firewallという言葉は某コンソールゲーム機のSDKを読んでいて初めて知ったのですが、それ以外の場所で滅多にお目にかからない言葉なので、あまりメジャーな呼び方では無いのでしょう。とはいえ、このようなADL対策について一言で言い表す表現を他に知らないので某SDKにならってADL Firewallと呼ぶようになりました。

まとめ

  1. 予期せぬADLの防止は型を別の名前空間に移して宣言する
  2. inline namespaceはADL対策にはならないので注意

コピー禁止Mix-in

大量のデータメンバを持つ大規模なクラスLargeObjectがあるとします。

そして、LargeObjectをメンバに持つクラスがあるとします。

このObjectHolderクラスを扱うユーザーが、LargeObjectにアクセスしようとした時にちょっとしたミスを犯します。

本来ならLargeObject& で受けなくちゃいけないのに、&を書き忘れてしまいました。そしてこのコードを書いたプログラマはそのミスに気づいていません。

コンパイルエラーになることもなく、実行時に粛々とLargeObjectのコピーが発生するだけです。またアクセサ経由で受けたobjに操作しても結果はholder内に反映されません。バグの温床ですね。

これはObjectHolderを書いたプログラマも本意ではありません。

しかし想定してない操作ができてしまうことが問題です。

クラスを書くということは、どういう操作を期待しているか、想定しているかをクラス自身に語らせる必要があります。

不測のデータコピーを抑止する一つの方法としてアクセサの戻り値は参照ではなく基本的にポインタで返すというプラクティスがあります。

また別の方法としてLargeObject自身をコピー禁止にするというのも手です。

クラスのコピーコンストラクタとコピー代入演算子をアクセス不能にすることでインスタンスのコピーを禁止することができます。

C++11より前の時代は=delete;による書き方が無いので、該当の関数をprivateにすることでコピー禁止を実現していました。

これで、先ほどの&忘れる事件もコンパイルエラーになり、不測のコピーが発生してもすぐに気づくことができます。

あるクラスのコピーコンストラクタ、代入演算子をアクセス不可にするとコピーは禁止できますが、第三者がそのコードを読んだ時に意図が分かりづらいという問題があります。

ですので、このコピー禁止の仕掛け部分を専用のクラスに分離した上で継承することで各クラスにコピー禁止属性を持たせるというアプローチが一般的です。

これでLargeObjectがコピー禁止になり、さらにクラスの定義を見ただけでコピー禁止だということが分かりやすくなりました。

コピー禁止についてはEffective C++でも説明されいます。

コンパイラが生成するコピーコンストラクタとコピー代入演算子は、その基底クラスに対応する関数(同じ関数)を呼び出そうとします。しかし、対応する関数を規定クラスでprivateにしておけば、その呼び出しはエラーになるわけです。

__Effective C++ 第3版

 

またboost::noncopyableにも同様の実装があります。

そしてMore C++ Idioms/コピー禁止ミックスインでも紹介されています。

これらの情報も併せて参照すると良いでしょう。

まとめ

  1. コピー禁止するにはコピーコンストラクタにアクセス出来なくする
  2. 代入演算子にもアクセス出来なくする
  3. 既定クラスを用意することでより意図を明快に出来る

Argument Dependent Lookup

C++言語の持つオシャレな機構の一つにADL(Argument Dependent Lookup)というものがあります。

和訳すると「実引数依存の名前検索」となり、読んで字の如く、「実引数」に依存した「名前検索」となります。なるほどわからん。

ADLの挙動

コードを見ながら順を追って理解してみましょう。

funcという関数が呼ばれるコードですが、これをただコンパイルするとエラーになります。

なぜならば……funcという関数は見る限りどこにも定義されていないからです!!

 

…………。

 

 

では、定義しましょう。

今度はコンパイルエラーにならずに無事、func関数が呼び出せます。

当たり前すぎて、今、自分が何を読んでいるのか分からなくなってきたのではないでしょうか。明日はどっちでしょうか。

先へ進みます。

func関数をnamespace ns1で囲みました。

このコードは再度コンパイルエラーになります。当たり前ですね。

9行目で呼ばれているfuncのシンボルをグローバルネームスペースから探そうとするのですが、実際に定義されているfuncはns1ネームスペースの中にある為、通常の名前検索ではヒットしない為です。

なのでネームスペースを指定した上で呼び出す必要があります。

まったく何を読まされているんだという感じでしょうが、ここまではウォーミングアップです。おつかれさまでした。

ではもっと具体的な実装コードを見てみましょう。

namespace ns1に色情報を持つ構造体と、内容を出力する関数を定義しました。

このmain関数をよく見て下さい。

Colorの型にはns1::という指定が付いていますが、printはネームスペースの指定を付けずに呼び出しています。

しかし、このコードはコンパイルエラーになりません。

なぜならc++様はある型のオブジェクトが関数呼出の際に実引数として用いられると、関連するネームスペースからも、その関数が探索されるという仕様だからです。

今回の場合は、printの引数で渡されているcの型がns1::Colorで、ns1ネームスペースに関係があるため、printという関数のシンボル検索グローバルネームスペースからだけではなく、関係があるns1からも検索するという挙動になります。

これが「実引数依存の名前検索(ADL)」の挙動です。

では「関連するネームスペース」とは何でしょうか。

引数がクラスのメンバだった場合

そのクラス自身および基底クラス、そしてそのクラスを囲むネームスペースです。

例えばstd::vector::iteratorはクラスのメンバですので、vectorとstdが関連するネームスペースになりますね。

引数がネームスペースのメンバだった場合

あるネームスペースの中で定義されているだけのクラスの場合、関連するネームスペースはそのクラスを囲むネームスペースです。そのクラスが継承していれば継承元のクラスに対して、同様の条件に基いて関連するネームスペースが決定します。

注意点

前述のとおりADLは人間の目では一見しただけではわからないネームスペースからもシンボルを探そうとします。

その結果、本来意図しない関数が呼び出される危険性もあります。

でも大丈夫、予期せぬADLの防止策もちゃんとあります。

ADL Firewall

また、この辺りの注意点について詳しく解説しているサイトも多いので色々参照してみるとよいでしょう。

google検索[ADL c++ 注意]

 

なぜ存在するのか

複雑性を増すだけに思えるこのADLという仕組みがなぜ必要なのか。それは演算子をネームスペースの修飾無しで呼べるようにするためです。

ADLという仕組みがないと

の部分を

と、何とも味わい深い呼び出し方をするしかなくなってしまいます。

これはストリーム演算子含むあらゆる演算子に言えることです。

よく登場する<<演算子も本来stdのネームスペースの中にあるので、ADLが無いと直感的に呼び出せないのです。

 

またSTLのswapのように(と言っても他に良い例を知らないですが)あえてADLの挙動を利用して汎用的な処理ができるようにしているコードもあります。

STLの中で呼ばれる修飾無しのswapはユーザー定義のものがADLで見つかればそれを呼ぶし無ければstd::swapが呼ばれるという実装になっています。

まとめ

  1.  ADLの挙動を理解しよう
  2. ADLによる名前解決の結果、実行するまでバグと分からないことがある
  3. ADLのお陰で演算子が完結に書けるというメリットもある
  4. 世の中にはADLの挙動を意図的に利用した悪魔的なコードもある

最も速い数値型

CPUには得意なデータ型というものがあります。

あるCPUでは4byteの数値計算が最も高効率に少ないクロック数で処理でき、また別のCPUでは8byteの数値計算が最も高効率に処理できる、といった具合です。

昔、世の中がまだ32bitCPUで溢れていた頃、ほとんどのCPUで最も高効率に計算できるデータ幅は32bit = 4byteでした。(あと8bit演算は常に高速でした)

その頃はintも往々にして4byteだったので、数値をやりとりする場合の引数、または戻り値の指定は単にintにしておけば良かったのです。

今でもそのころの名残で、慣例的に引数、戻り値をintで受ける書き方をしている人も多いのではないでしょうか。

 

時代は変わりました。

 

今やどこを見ても64bitCPUで溢れています。

にも関わらず、多くの環境でintは4byteのままです。

今の時代で引数に盲目的にintを指定することは、環境の変化に対応しきれていない可能性があります。

幸いにも、どの環境でもなるべく高効率になる数値型を標準が用意してくれています。

fastの後ろの数値が最低限保証するbit精度です。

例えばuint_fast8_tは「最低でも8bitの精度が保証された中で最速のデータ幅」を持つ符号無し整数を表します。

0~100までの数値を引数で受け取って10倍にして返すという、(なんの使い道もない)関数を定義してみましょう。

精度が保証されているだけで8bit整数(std::uint8_t)と同じbit幅とは限らない点に注意して下さい。std::uint_fast8_tのデータ幅は16bitかもしれませんし、64bitかもしれません。

参考までに手元の環境における各型のサイズを示します。

int_fast8_t 1byte
uint_fast8_t 1byte
int_fast16_t 8byte
uint_fast16_t 8byte
int_fast32_t 8byte
uint_fast32_t 8byte
int_fast64_t 8byte
uint_fast64_t 8byte

 

まとめ

  1. cstdintに「Nbitの精度を保証して、かつ処理効率が最速になる型」というものが定義されている
  2. その型で保証している精度と実際のデータ幅は往々にして異なるのでデータ幅に依存したコードは書いてはいけない

 

 

ポータビリティの高い数値型

数値型の別名定義を用意しているプロジェクトは多いです。

符号付か否か、あとは型の持つbit数などを踏まえたエイリアスを用意することでタイピング数の削減と統一的な名前規則でコードを分かりやすくしようという意図が感じられます。

一定規模以上のプロジェクトになると、ほぼ必ず上記のような型の再定義ファイルがあるように感じます。既視感のある人も多いのではないでしょうか。

ただ、上記の例は非常にポータビリティに欠ける為、行うべきではありません。

ユーザーが「intの別名はs32」と定義したところで、実際にs32のサイズが32bitになる保証はどこにもありません。

サイズが保証された数値型を標準が用意してくれているので、そちらを使うようにしましょう。

どんな環境でもstd::int8_tは8bit符号付き整数を表すことが保証されています。

 

アドレス型のサイズとintのサイズが違う環境も多いので、アドレスを数値として計算する場合は気をつけましょう。例えば、64bitCPUの場合、アドレスの値の範囲は64bitですがintのサイズは32bitというケースも多いです。

obj_aのアドレスとobj_bのアドレスの差分を計算するコードですが、例えばポインタが64bitなのにintは32bitだった場合、サイズオーバーのため正しい計算結果が得られない可能性が高いです。

なのでポインタ(アドレス)を数値として計算する場合は、ちゃんとアドレスと同じ値の幅を持つことが保証されている数値型std::intptr_t、もしくはstd::uintptr_tを使いましょう。

 

あなたが一生懸命作ったスマートフォン向けのゲームを3DSに移植したら全然動かなくなっちゃった!とならないためにも日頃からポータビリティを意識したコードを書くようにしましょう。

まとめ

  1. 値の幅が保証された標準の数値型を使おう
  2. アドレスの計算にはアドレスの幅が保証された標準の数値型を使おう

 

配列の要素数の取得方法

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

要素数が変わったら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. メモリレイアウトが不定になるシチュエーションを知ろう

レガシー環境で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を書けるような最新の型推論を備えたコンパイラが使える環境に転職しよう

真に安全な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ポインタに対応しなければいけない