🌟

お手軽型破壊

2025/02/26に公開

C++には強力な型システムがあります
普段からその恩恵に授かっているユーザーは多いことでしょう
しかしC++は自由度の高い言語でもあり、型シグネチャの保障は絶対ではありません
何故ならC++には型キャストという宝刀があるからです

参照型

C++には左辺値型右辺値型という少し変わった型の概念があります

int &n=*new int;
int &&n=0;

左辺値は=の左辺に適用可能なデータ領域そのものです
言うなればポインタから参照可能な全てのインスタンス領域です
変数は既定で左辺値型を返すように動作しており、他にも左辺値型を返す手続きはいくつか存在し、自身でも定義できます

右辺値は=の右辺に渡せる定数です
一時オブジェクトとしてメモリ内に格納されるデータを指しており、通常参照することはできない領域に保存されます
リテラルや関数の戻り値など、通常あらゆる値は右辺値型を返します

参照キャスト

ここで大事なポイントがあります
それはこの両者が共にであるということです
型であるとはすなはちキャストの対象であることを指します
抽象化されたデータ領域の本体とも言える型にキャストするとは、何を意味するのでしょう
ここではそれを便宜的に参照キャストと呼称します

型を破壊しよう

物騒な項題ですがこれは実例を示した方が早いでしょう
参照キャストを行うと以下の操作が可能となります

C++
int main(void){
	float f=.1F;
	int &i=(int &)f;
	std::cout<<i<<std::endl;
};
Console
1036831949

float fの戻り値をint &に変換したことで、本来は浮動小数を表現するはずのデータ領域がintの左辺値としてint &iに参照されています
つまり、左辺値型にキャストしたことで、浮動小数のエリアが整数型の変数に渡されています
この状態では左辺値参照が成立しているのでiの値を書き換えることでfの値も変化します

C++
int main(void){
        float f=.1F;
        int &i=(int &)f;
        i*=2;
        std::cout<<f<<std::endl;
};
Console
1.59507e+36

キャストの対象は変数だけではありません
キャストはあらゆる値を対象とするので、式の戻り値やリテラルにも参照キャストを適用できます

C++
int main(void){
	int &i=(int &)(void *&&)new float(20.0);
	std::cout<<i<<std::endl;
};
Console
-1143132496

これはポインタを参照キャストしたことで整数変換されたアドレスがintの範囲でオーバーフローしています
またこの式では一度右辺値参照を通したことで一時オブジェクトの領域が参照されるため、本来は戻り値である値に対して自由に書き換えが行える状態です
これを利用して、一時オブジェクトの状態遷移を観測するような以下のコードを考えられます

C++
int *run(int &&i){
	std::cout<<i<<std::endl;
	return &i;
};

void $case(int &&){};

int main(void){
	int *i=run(0);
	$case(5);
	std::cout<<*i<<std::endl;
	int &&b=3;
	std::cout<<*i<<std::endl;
};
Console
0
5
3

式の戻り値を保存することで書き換えられる一時オブジェクトの情報をint *iが受け取っています
これにより、何も操作を加えていないはずのデータ領域の値が、その後のコードの進行によって変化する様子が伺えます
特に

C++
int *run(int &&i){
	std::cout<<i<<std::endl;
	return &i;
};

というコードは、意識していなくとも書いてしまいそうな実装です

参照の安全性

C++であれば普段何気なく扱っている参照型は、ポインタより信頼性が高いものとして普段から推奨されることも多いデータ型です
しかし$case(int &&)のような例を見れば、その限りでないことが分かります
ポインタ型は参照外しの適用時に*が必要ですが、参照変数にはその制約がありません
むしろポインタよりも手軽なキャストを許容し、破壊的な操作を容易にする型とも捉えることができます

一概に「ポインタ=危険」と流布するのも、考えものかもしれませんね

Discussion