ポインタ型記法のススメ ─ int* p; int *p; 空白をどちらに挿入するか

C言語におけるポインタ変数の書き方には複数の記法あります。

char* p = "s"; // ① charポインタ型の変数p
char *p = "s"; // ② char型のポインタ変数p
書き方や言い方が違うだけでいずれも全く同じ意味で同じ動作をします。

私自身はポインターの前にスペースを挿入する②char *p、いわゆるポインタ変数記法を支持していますが、最近は①char* pポインタ型風の記法も悪くないと思うようになりました。そこで今一度ポインタ型記法の有効性を検証してみましょう。

ただし本来、ポインタは変数に対して与えられる概念であるため、アスタリスクは型側ではなく変数側に寄せるのが自然です。

目次

ポインタ型記法のメリット

文字数の削減ができる

ポインタ型記法で統一すると文字数を若干削減できます。 型キャスト時に有効な記法であることがわかります。

// ①
- (String*)substring:(String*)sub start:(Array<Number*>*)start end:(Array<Number*>*)end {
  String* str = (String*)that(sub, start, end);
  return it(str, sizeof(void*));
}

// ②
- (String *)substring:(String *)sub start:(Array<Number *> *)start end:(Array<Number *> *)end {
  String *str = (String *)that(sub, start, end);
  return it(str, sizeof(void *));
}

typedefとの相性が良い

// ①
typedef const char* string;
// ②
typedef const char * string;
typedef const char *string;

②の方法は四則演算の文法と似ており目的や意味が不明確となります。かといって*stringを用いてしまうと誤釈や余計な解釈を引き起こすことになります。

①の記法で統一すれば余計な混乱は無くなるでしょう。

constポインタとの相性も良い

また、constポインタを宣言する際も記法にブレや迷いが生じません。

// ①
char* const string;
// ②
char *const string;  // ポインタ変数記法が破綻している
char * const string; // 人によって書き方が異なる場合も

同様に、Clangで用いられているNullability関連の属性との相性も良いです。

char*__nonnull str;
char *__nonnull str;

統一性や一貫性の観点で見ると、とても有効な記法であることがわかります。

宣言と参照を区別することができる

ポインタの値を参照する際に間接演算子*を用いることがありますが、この記法はポインタ変数宣言時の記法と被っています。

Bar foobar(Foo *foo) { return *foo; }

*fooが二回出現しています。それぞれ異なる用途であるにもかかわらず、同一の記法が使われているのです。ソースをより直感的なものにしたいなら、以下のようにポインタ型スタイルで両者を区別させると良いでしょう。

Bar foobar(Foo* foo) { return *foo; }

このような区別はソースコード検索時に大変有効です。*fooと検索すれば、ポインタ変数への値参照処理のみを抽出できますし、* fooと検索すれば仮引数宣言や変数宣言の箇所のみを抽出することが可能になるのです。

型情報とシンボル名が綺麗に区分けされる

char* read(char* path) {} // ①
char *read(char *path) {} // ②

ポインタ宣言子*はある種の型情報と言えなくもありません。それを型と切り離し、あまつさえ変数側に寄せるなど、不自然極まりない酷薄の所業と言えるでしょう。

①の方法を取ることで、型情報(char*)とシンボル情報(read, path)を空白/スペースによって明確に区分けする事ができます。

メリット

①の方法によって、関数名や変数名を認識しやすくなるというメリットが生まれます。変数名に付いていた余計なホクロ*が取れ、シンボル名がよりくっきり明瞭になりました。視認性の向上が期待できます。

デメリット

ただし、変数名を見ただけではそれがポインタ変数なのか、普通の変数なのかが直感的に理解できなくなくなるというデメリットが生まれます。従来の記法に慣れてしまったプログラマにとっては判読性が下がることになります。

②のポインタ変数記法に慣れている熟練プログラマほど、これをデメリットと感じる割合は多くなるはずです。逆にJavaやSwift等のポインタ宣言子が存在しない言語に慣れているプログラマは、むしろ①の記法の方がしっくりとくるかもしれません。

ちなみに

若干趣旨は異なりますが、Javaでは配列型の宣言を型側に記述することが推奨されています。

String[] strings; // 推奨
String strings[]; // 型と配列宣言子が離れすぎている

色々な言語に触れていると、やはり型情報とシンボル名は明確に区分けされていたほうが直感的だと感じることが多いです。

ポインタ型記法のデメリット

int* a, b;
int x = 0;
a = &x; // OK
b = &x; // Error! "Assigning to 'int' from incompatible type 'int *'; remove &"

上記の変数bはポインタ変数ではなく通常のint型変数として解釈されています。コンパイラはint*宣言をポインタ型のintとしては解釈しません。*はあくまで変数aの物として解釈されるのです。

// 理想
(int*) (a), (b);
// 現実
(int)(* a), (b);

変数bをポインタにするためには以下の様に、変数bに対してもポインタ宣言子を用いる必要があります。

int* a, *b;

アスタリスクによるポインタ宣言子はあくまで隣り合う変数に対してのみ適用されます。 ポインタは型情報ではなく、あくまで変数に対して適用される概念なのです。

これはポインタが変数のアドレスを保持する変数であるという解釈にも関係しています。

結局の所、C言語の世界にはint*などというポインタ型は存在しないのです。

C言語でポインタ型を実現する方法

とは言っても、実はtypedefを用いることでポインタ型の表現が可能となります。

typedef int* int_p;

// a, b両方共int*型になる
int_p a, b;
int x = 99;
a = &x; // OK!
b = &x; // OK!

またtypeofを用いる方法もあります。

typeof(int*) a, b;

int x = 99;
a = &x; // OK!
b = &x; // OK!

そもそもポインタ型記法を使ってる人はいるのか

残念ながら、熟練のC言語使いでアスタリスクを型側に寄せる人達を全く見たことがありません。これは「ポインタは変数に対して適用される概念だ」という考えが根底にあるためだと思われます。

ただし、C++使いにはポインタ型スタイルを採用する人たちが意外と多いように思います。そもそもC++では参照型の&を型側に詰めるスタイルを取ることが多いため、ポインタにおいても同様のスタイルが自然と受け入れられているように思います(これらの文化はC++のテンプレート機能とも深く関係しているように思える)

// C++サンプル
void foobar(Foo& foo, Foo* bar) {
   Bar* a = static_cast<Bar*>(bar);
   Bar& b = static_cast<Bar&>(foo);
}

template<class T> void typeof_typename(int v) {
   T a, b;
   a = &v; // OK!
   b = &v; // OK! // bはきちんとint*として解釈されている
}
typeof_typename<int*>(9);

有名プロジェクトでの使用例

Linuxカーネル、Vim、Emacsのような古くからあるC言語系のプロジェクトでは従来のポインタ変数スタイルが採られています。

逆にWebKitやBoost、人工知能関連の比較的新しいC++系プロジェクトではポインタ型記法が使われていることが多いようです。

ポインタ型派(int* ptr)

  • Boost, WebKit(C++)
  • Apple社のC++コード(JavaScriptCore、dyld、ld64)
  • Microsoft社のC++コード(ChakraCore、CNTK、WinObjC)
  • Google社のC++コード(GoogleTest、Skia)
  • その他C++プロジェクト(Chromium, Electron, Caffe, TensorFlow)
  • その他C言語プロジェクト(Kuin)

ポインタ変数派(int *ptr)

  • Linux, Git(C言語)
  • Vim, Emacs(C言語)
  • Ruby(C言語)※一部ポインタ型あり
  • GCC(C言語 ※C++コンパイラでビルドされている。C++のソースもあり)
  • LLVM, Swift(C++)※参照も変数側に寄せている
  • Google社のC++コード(mozc、Google-Glog ※一部ポインタ型あり)
  • Apple社のC言語コード(低レイヤー系のほとんど)

混在派

  • HHVM(C++, Facebook社)

HHVMについては、参照・ポインタ共に混在しています。ファイル単位ではなく、関数単位で混在しています。コントリビューターが非常に多いプロジェクトだという特徴があります。

C言語系のプロジェクトは基本的にポインタ変数派が多く、C++系のプロジェクトではポインタ型派が多い傾向にあるようです。両者の違いは、やはり文化的な違いから来るもののようです。

Rubyの場合はテストケース用のコードでポインタ型記法が使われている程度です。

GCCは一部C++が取り入れられている特殊なプロジェクトなのですが、ポインタ変数とポインタ型記法が混在しています。ただし基本はポインタ変数のようです。

まとめ

  • C言語の世界ではポインタ変数スタイルが基本
  • そのためポインタ型記法は非論理的な印象が強い
  • しかしポインタ型記法は実用面では合理的なメリットが多い
  • C++の世界ではポインタ型記法が取られることも多い

番外編

ダークホース現る

char*read(char*path){} // ③

普段私がテキストエディタやターミナルで簡単なコードを書く際に使う記法です。 慣れると意外と読みやすい事に気づきます。あまりオススメはしませんが。

広告

ポインタ型記法のススメ ─ int* p; int *p; 空白をどちらに挿入するか」への1件のフィードバック

  1. ピンバック: ダークサイド Objective-C コーディング規約 | MaryCore

コメントは停止中です。