teppy のすべての投稿

newの実装

newは引数でサイズを受けますが、標準のnewはsizeが0でも正しく動作することを規格で定めています。

自分で実装する際には意識するとちょっとだけ規格に準じたコードになってよいです。

これ豆な!

まとめ

  1. 標準のnewはサイズ0にも対応している
  2. 完全に準じるにはそこそこの知識が必要なので、せめてこれだけでも覚えておくと良い
  3. そんなレアケースに対応することでnewにコンペア命令が増えるなんて勿体無い!という人も教養として覚えておくと良い

Empty Base Optimization

「空のクラス」を定義します。

Sky classという意味ではありませんよ。Empty classという意味です。

メンバ変数を持たず継承もしていない、とにかくデータとしての要素の無いクラスです。

このEmptyClassのサイズはいくつでしょうか?

Emptyという位なのでゼロなのかと思いきや、sizeofで計るとゼロ以外の数値になります。

Wandboxで見てみましょう

sizeof(Empty) = 1ですね。

なぜか?その答えはある名曲に隠されています。

言葉はいつも奥の方から後ろに虚しさ連れて教えてくれた
けれどこんなにもからっぽになったのに僕は歩き出した

__ゆず「からっぽ」

からっぽに見えても、みんな何かを抱えて生きているのです。ゼロなんてありえないんですね。C++のクラスも同じなんですね。

納得感を得たところで、改めて言うと、C++の型は全くデータメンバが無い状態でも単独でインスタンスが作られる場合必ずサイズを持ちます。

C++のテクニカルな理由で、「空のクラス」でも、「独立したオブジェクト」のサイズは0にできないのです。

__Effective C++ 第3版

「テクニカルな理由」というのは、インスタンス化された際にオブジェクトのアドレスをとれるようにする為です。サイズ0ではアドレスが決まりませんからね。

このようにc++では単独でインスタンス化されるオブジェクトは例外なくサイズを持ちます。

例えばメンバ変数の一部として定義された場合もサイズを持ちます。

この場合でも&Hoge::eを決めなければいけないのでeはサイズを持ちます。結果として sizeof(Hoge) > sizeof(int) となります。

たった1byteとはいえ無駄なメモリを消費することになります。

コピー禁止をmix-inするboost::noncpyable等はまさにメンバを持たない空のクラスですね。コピー禁止するたびにデータサイズが増加したらたまったものではありません。

そんなわけで、規格では、空の型が別の型を継承していて、継承元の型が既にサイズを持っている場合や、逆に空の型の継承先がサイズを持っている場合はEmptyClass自体の追加のサイズは無くしても良いことになっています。

 

型を継承した時に、追加のサイズを持たせない最適化のことをEBO(Empty Base Optimization)と言います。

EBOの効かないコンパイラなんてほとんどありませんが、場合によっては型サイズの肥大化やアライメントのズレの原因になるので注意する必要があります。

EBOを意識した実装

コンパイラが空のクラスを最適化してくれることは理解しました。

プログラマもEBOが効くコードを意識して書きましょう。

先のboost::noncopyableも空のクラスでしたが、他にも空のクラスを定義するシチュエーションは数多くあります。

例えばファンクタなんかも、往々にしてoperator()だけ定義して、メンバ変数も特に無いというケースがあるかと思います。

スマートポインタのデリータやコンテナのアロケータを渡して実装を決めるテンプレートクラスもEBOを意識した設計にすることでメモリ効率が上がります。

以下が一例になります。オリジナルのコンテナクラスにテンプレートパラメータとしてアロケータを渡す実装で考えてみましょう。

まずはEBOを意識しない設計。

普通にAllocatorをメンバとして持っています。

続いてEBOを意識した設計。

Allocatorを継承した設計になっています。

 

サイズを計ってみましょう。Wandboxで確認

このようにちょっと意識することでメモリ効率の良いクラス設計ができるようになります。

 

まとめ

  1. C++では単独でインスタンス化されるオブジェクトは全て1以上のサイズを持つ
  2. サイズを持つ理由はアドレスを決定できるようにするため
  3. 他にサイズを持っている型と継承関係を持つことで空の型のサイズは最適化される
  4. この最適化をEBOという
  5. ちょっとEBOを意識したクラス設計にすると「解ってる感」が演出されて良い

 

禁忌の識別子

名前を付けるという行為には責任が伴います。

子供にせよ、ペットにせよ、変数名にせよ。

ドラクエ5で子供の名前を付ける時に、かなり悩んだ経験を持つ人も多いのではないでしょうか。

そしてC++でコーディングしている時も名前を付ける機会は多いです。

私の人生で、何かに名前を付けるという機会が数多くありましたが、その99.999%はプログラミングしている時でした。

関数名、変数名、マクロ名にクラス名……他にもとにかく名前を付けなければいけないタイミングは多いです。その都度、ドラクエ5の時くらい悩んでいてはプロジェクトは全く進まなくなってしまうので、直感と経験でサクサク名前を付けましょう。

ちなみにここで言及している「名前」のことをC++的に気取っていうと「識別子(Identifiers)」といいます。

この識別子には色々ルールがあって、ルールに違反した識別子は使ってはいけません。

エラーになる識別子

識別子のルールに違反した場合、コンパイラがエラーにしてくれるものがあります。

  • 数字から始まる識別子は使えません
  • C++キーワードとして登録されている文字列は識別子には使えません
  • 一部演算子の代替表現も識別子には使えません

 

一部演算子の代替表現?なんだそれは!?

C++ではキーワードではないが、ある種の演算子と同意語の単語がいくつか用意されており、それらも識別子として使用できません。

例えば && の代替表現は and で、 | の代替表現はbitor です。

Furthermore, the alternative representations shown in Table 5 for certain operators and punctuators (2.6)
are reserved and shall not be used otherwise:

Table 5 – Alternative representaions

and and_eq bitand
bitor compl not not_eq
or or_eq xor xor_eq

__n3337 2.12 keywords.

 

 

これらのルールはコンパイルの段階でエラーになるので、詳細を覚えててなくても大丈夫です。

 エラーにならない識別子

以下のルールはエラーにはなりませんが、C++の予約語のため、使ってはいけません。

  • アンダースコアで始まり大文字が続く名前(コード中どこでも禁止)
  • アンダースコアが連続する名前(コード中どこでも禁止)
  • アンダースコアから始まる名前(グローバル変数のみ禁止)

これらのルールに違反した識別子を使った場合の動作保証は無いので、絶対に使わないようにしましょう。例え__Japan_987654321_wwww___ssl93jjasf__なんて識別子が被るなんてあるわけないから別に問題ないじゃん!と思ったとしても、避けられるリスクは避けるべきです。単に言語の規格で使うべからずとされているものは使わない方がいいのです。

特にインクルードガードを書く時にこのルールを犯しているコードをよく見かけます。

コンパイラがエラーにしてくれないので、書く側で気をつけましょう。

まとめ

  1.  キーワードと同じ名前は識別子にはつけられません
  2. 記号の代替表現と同じ名前は識別子にはつけられません
  3. 数字から始まる名前は識別子にはつけられません
  4. 予約語となっているルールを破る識別子は絶対につけてはいけません

インクルードガード

c++の#include ディレクティブは、プリプロセスで解釈され、書かれたファイル名の内容を単にその場所に展開したものとして処理されます。

 

一般的にファイルの先頭付近にinclude文はまとめて書く感じですが、コードの中に埋め込むことも可能です。

たとえば以下のような、動物の名前を列挙しただけのファイル(animals.csv)があるとします。

馬、狼、カンガルーと書いてあるだけですが

とすると、

と展開されて、普通にコンパイルが通ります。

このように#includeディレクティブはとにかく機械的に展開するものだ、という話です。

で、例えば普通に同じヘッダーファイルを複数includeしてしまうと、(機械的に展開するだけなので)includeの数だけその場所に展開されます。include元のファイルに何かシンボルの定義みたいなものが含まれている場合、多重定義エラーになります。

hoge.hpp

main.cpp

なので、一つのコンパイル単位中に同じファイルが複数回、展開されないようにする必要があります。

多重に展開されることを防止するには、展開元のファイルに多重展開防止用のマクロ(インクルードガード)を仕込むのが定石とされています。

c/c++が標準で提供する全てのヘッダーファイルにはインクルードガードが使われています。

インクルードガード

シンボルの定義名(例ではINCLUDE_GUARD_HOGE_HPP)は、一意に定義される文字列なら別に何でも良いです。

複数includeされても、最初の一回以外はINCLUDE_GUARD_HOGE_HPPが定義済みなので単にスキップされますので、include元のファイル内に書かれたクラス定義などが多重定義されてしまう心配はありません。

冗長インクルードガード

先ほどの例のようにinclude元のファイルの先頭と末尾を#ifndef -#endifで囲うインクルードガードを、厳密には「内部インクルードガード」といいます。

そして、この#ifndef -#endifの部分をinclude先にも書く手法を「冗長インクルードガード」といいます。

このように書くと#include文の評価をする前に、スキップするか決められる為、コンパイル時間の短縮につながる場合があります。

困ったことに、コンパイル時に複数のヘッダーファイルが何度も検出されるような状況下では、ヘッダーファイルを開き、#ifndefを評価し、終わりの#endifを走査するプロセスに時間がかかる可能性があります。場合によっては冗長インクルードガードを設定することにより、コンパイル時間を大幅に短縮できます。

__C++標準的コーディング技法 #62:インクルードガード

冗長インクルードガードについては毎回外部に書く手間の割に大したメリットも無いのであまり使われていません。むしろガードにつかうマクロ名を内部インクルードガードと冗長インクルードガードで合わせなければいけない為、別々のファイルなのに結合が生じてしまいます。内部インクルードガードのマクロ名を変えた場合、冗長インクルード側のマクロ名も忘れずに変えなければなりません。

外部の#includeガードは冗長で、現在のコンパイラ上では時代遅れである。また、インクルードする側とヘッダーファイルがガード名について合意していなければならない。これにより、密接な結合が生じ、脆弱となる。

__C++ Coding Standards

今の時代には不要となった冗長インクルードガードですが、教養として押さえておくと良いでしょう。コモンセンスってやつですね。

#pragma once

言語機能ではありませんが、多くのコンパイラではこの魔法の言葉をファイル中に書いておくだけで複数インクルードされない仕組みを提供しています。インクルードガードを書くより、遥かにタイプ数が少なく済むので単純に楽です。

しかし、#pragma onceによる複数include回避の方法はコンパイラに依存するため、コンパイラによっては使えないこともありますし、コンパイラの実装によっては予期せぬ不具合の原因になる場合があります。

例えば、あるファイルを複数includeしない為に、そのファイルが既にincludeされたことがあるかをコンパイラは覚えておく必要があります。include済みのファイルかどうかを判定するために、ファイル単位に一意のシンボル名を定義するとします。その一意の文字列生成の為にファイルパスを埋め込んでシンボル名を作るような実装になっていた場合、すごーーーく長いパス(c:/user/xxxxxxxxxxxx/development/my_project/tekitou_na_project_name/game/lib/include/group/category/tekitou_na_header_file_name.hpp)の場合、途中でバッサリとパスが切られて、まったく同じシンボル名になって、初めてincludeしたファイルなのにスキップされるという、なかなか原因解明が困難な不具合に襲われる……といったケースがあるかもしれません。

……はい、体験談です。

もともと、言語標準じゃない機能なので私は使わなかったのですが、あの一件以来、さらに避けるようになりました。

コンパイラによる拡張機能で、言語の機能ではないので、これをコード中に書いた瞬間に、そのコードはコンパイラ依存のコードになります。ポータビリティの高いコードを書きたい場合は、単に使わない方が得策です。

#pragma onceを使わなくても、マクロによるインクルードガードで、コンパイラ依存の無い形で実現出来ますからね。

#pragma onceも冗長インクルードガードと同様、includeするファイルの中を見に行く前に、以前にincludeしたことがあるかを判定出来るため、若干コンパイル時間が短くなることが期待できます。

実例

初学者はC++の勉強の一環として、CryENGINEのソースコードを読んだりすると思いますが、その中でもこのインクルードガードに触れることができます。

以下はCryENGINEのあるヘッダの冒頭部分です。

インクルードガードのマクロ名が__で始まっていますが、連続するアンダーバーを含む名前はC++の予約語なので真似するのは絶対に止めましょう。

参照:禁忌の識別子

スーパープログラマ集団でも平気でC++の規格に違反してしまうところを垣間見て勇気を得た所で先に進みます。その下でVCのバージョンを見た上で#pragma onceしているのが分かると思います。

#pragma onceが効く環境では#pragma onceによって、ちょっとでもコンパイル時間を短縮しようというという貪欲な姿勢が見てとれますね。このようにインクルードガードとの合わせ技で攻めるというアプローチもあるので覚えておくと良いかもしれません。

 

まとめ

  1. #include ディレクティブはプリプロセスでファイルを機械的に展開するだけ
  2. 多重includeを防止するためにインクルードガードを書こう
  3. インクルードガードには内部と冗長の2種類がある
  4. コンパイラ拡張の#pragma onceによっても多重includeは防止できる
  5. (おまけ)インクルードガードに使う定義名はc++のルールの中で決めよう

Plain Old Data

C++におけるPOD(Plain Old Data)とは、C言語のデータと互換を持つデータ構造のことです。

memcpyでデータをコピーできたりするものですが、それは本質的ではなく、とにかくPODの一番の意味はC言語のデータとbitレベルで完全に互換を持つということです。C++とC言語をまたぐプログラムがある場合、受け渡されるデータはPODでなければいけません。

会話中での出現例でいうと「あれ、ちゃんとPODで作っといてよ!」という具合に使います。

では、PODとは具体的にはどういうデータ構造でしょうか。

C互換データ=PODは、複雑なクラスレイアウトや、構築、コピー、ムーブなどのユーザ定義のセマンティクスを配慮することなく、”単なるデータ”として利用できるオブジェクトである。

__プログラミング言語C++第4版 8.2.6 POD

POD型の定義

  • 標準レイアウト型(standard layout type)である
  • トリビアル型(trivial type)である

この2つの定義を満たせば、それは紛れもないPOD型です。

簡単ですね!

それぞれの定義をさらに掘り下げます。

標準レイアウト型

  • virtual関数を持たない
  • virtual継承をしていない
  • 参照メンバを持たない
  • 非staticメンバに対して複数のアクセス指定子を持たない(重要!)
  • 標準レイアウトでない型を継承していない
  • 標準レイアウトでない非staticメンバを持たない
  • 継承していてもいいが非staticメンバを持つクラスは継承ツリーの中で1つだけしかない

4番目の項目に注目してください。クラスの不定なメモリレイアウトでも触れたとおり、複数のアクセス指定子でメンバを定義してある場合、標準レイアウトの保証はなくなります。

トリビアル型

  • トリビアルなデフォルトコンストラクタを持つ
  • トリビアルなコピー動作およびムーブ動作をする

散々登場しているこの「トリビアルな○○」ってなんやねん!と、あなたは言うでしょう。分かっていますよ。

トリビアルなデフォルトコンストラクタを持つ、トリビアルなコピー動作、ムーブ動作をするとは、大雑把に言うとコンパイラ定義のこれらの動作をするということです。

例えば、コピーコンストラクタをユーザーが定義したら、それはもうトリビアルな型ではありません。

引数無しコンストラクタをユーザーが定義したら、コンパイラ定義のデフォルトコンストラクタが無くなるのでこれもトリビアルな型ではありません。

 

さあ、これで自分の書いたクラスがPODかどうか判断できるようになりました。でもこんなことは忘れてもいいのです。

あるクラスがPODかどうか、本当に知りたいのならstd::is_podを使いましょう。

 

(コンパイラベンダーの実装にバグが無い限り)正確にPODの判定をしてくれます。

 

小難しいことぁいいんだよ!という人へ。

それほど間違っていないPOD型

  • 自分でコンストラクタとか定義してない
  • virtualという文字列を打ってない
  • 継承してない
  • メンバ変数はprivate(もしくはpublic)にしか定義してない
  • 組み込み型(int, char, float…etc)しかメンバに持ってない

とりあえず、これを抑えておけば大概はPODです。それでもPODじゃないデータ構造を定義してしまったら、逆にすごくね?と思うようにすると良いでしょう。

これはPOD。

これもPOD。

あと、おまけですがPODじゃない型のstaticメンバを持っていてもそのクラスのPOD性には影響ありません。

まとめ

  1. PODはC言語互換のデータ構造
  2. PODの定義は細かく定められている
  3. c++11ボーイエンドガールズはstd::is_pod<T>::valueで判定しよう

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. アドレスの計算にはアドレスの幅が保証された標準の数値型を使おう