参照とポインタの違い

参照とポインタの機能面の違いというのは良く知られているかと思いますが,コード生成の違いに関しては結構知らない方が多いのかもと思い解説します。
記事が大変長くなってしまいましたが, 個人的には参照は非常に面白い物だと思っていましてそれが伝わればと思います。もし何か間違いがあれば誰か教えて下さい。


まず, 参照とはつまりエイリアス(別名)だというのは良いかと思いますが,それをどう実現するかに関して規格は一切言及していないんですね。どうやるかはコンパイラの自由です。

具体的にどういう実現方法があるかというと, unionやハードウェア固有の機能の利用(特殊なメモリとか)もありえますが,現実的には

  • ポインタ
  • 名前置換 (分かり易く言うと #define x y みたいなただの置換)

がほとんどだと思います。
C++の参照は再代入不可, アドレス取得不可等の制約があるため簡単な解析で後者にも使えるというのがポイントで, Javaなどの参照とは異なっています。

そして, コンパイラはポインタなんて極力使いたくないので, 可能な場合は置換で参照を処理します。

では, どういう場合にポインタを使用するかというと他の関数の引数として渡す場合等, そうせざるを得ない場合です。例えば

void
pair_swap(std::pair<int, int> &p)
{
    int t = p.first;
    p.first = p.second;
    p.second = t;
}

void
pair_swap(std::pair<int, int> *p)
{
    int t = p->first;
    p->first = p->second;
    p->second = t;
}

は通常はほぼ同じアセンブリになると思います。

次にこれらがインライン化された場合を考えてみます。

参照の場合

std::pair<int,int> x = std::make_pair(0, 1);
pair_swap(x);
std::cout << x.first << x.second << std::endl;

std::pair<int,int> x = std::make_pair(0, 1);
std::pair<int,int> &p = x;
int t = p.first;
p.first = p.second;
p.second = t;
std::cout << x.first << x.second << std::endl;

になります。すると,もはやpがポインタである必要がないので,ただの置換で処理します。

std::pair<int,int> x = std::make_pair(0, 1);
int t = x.first;
x.first = x.second;
x.second = t;
std::cout << x.first << x.second << std::endl;

次に, 全てのアクセスがx.firstかx.secondの形なので, xに連続領域を割り当てる必要がない事が解析され,pairはバラバラに分解されます。

int x_first = 0;
int x_second = 1;
int t = x_first;
x_first = x_second;
x_second = t;
std::cout << x_first << x_second << std::endl;

最終的に定数畳み込みで

std::cout <<  1 << 0 << std::endl;

になります。こんな感じで参照を利用すると, 他のコード変換の効果が向上する場合が結構あります。

ポインタの場合

インライン化すると

std::pair<int,int> x = std::make_pair(0, 1);
std::pair<int,int> *p = &x;
int t = p->first;
p->first = p->second;
p->second = t;
std::cout << x.first << x.second << std::endl;

となります。さて, これの最適化は困難です。pは再代入などが可能なので, ただの置換では処理できません。より最適化をかける為にはエイリアス解析という高度な解析が必要になります。

g++の場合は, この程度の単純なコードであれば-O2で両者とも全く同じコードになりますが, コンパイラの苦労度合は全然違うんですね。
エイリアス解析は完全に行う事が困難なので,分岐が入った複雑なコードになると-O2でも差が現れてくると思います。


まとめですが, 参照は可能な場合は単なる置換で処理されるので生成されるコードも変わってきます。もっと面白い事をしているコンパイラもあるかもしれません。

ここに書いたのは参照の実現の一形態であって, 処理系によって異なるということに注意しておきます。少なくともg++では以上のような感じになっているようです。[追記]これは調べなおします。