はじめに
同じメモリ領域に別の型を格納したい, 同じメモリ領域を初期化したときとは別の型で読み出したい, シリアライザの実装などで必要になることがあると思います.
解決策として, 共用体を悪用活用したり, ポインタの指す型を読み替えたりする違法行為が行われてきました (JPCERT EXP32-C).
共用体の誤った使い方
次のコードはC言語の場合は未規定で動作しますが, C++では未定義です.
union U
{
int32_t i_;
float f_;
};
U u;
u.i_ = 0;
float f = u.f_; //< 最後に値を入れたメンバ以外でアクセスすることは, 未定義
型の読み替えの誤った使い方
ポインタの指す型が互換性のない型の場合, 未定義動作になります.
int32_t i=0;
float f = *((float*)&i);
タイプパニング
型変換ではなく, ビット単位で同じ内容の別の型に変換することを, タイプパニング(type-punning)といいます. 共用体やポインタの型を読み替える方法は, 多くの処理系で期待したとおりに動作してしまいます(全ては調査していないです).
安全な方法を次に記載します.
C++17以前
memcpy
を使う方法があります. シリアライザなどで, エンディアンを入れ替える場合は, バイト毎に入れ替えてからmemcpy
を行うことになるでしょう.
コードの面倒くささはどうしようもないですが, パフォーマンスに関してはコンパイラ組み込み関数を使うと型の読み替えと同等になると思います.
#include <cstdint>
#include <cstring>
#include <iostream>
int main(void)
{
int32_t x=0;
float f;
::memcpy(&f, &x, sizeof(f));
std::cout << f << std::endl;
return 0;
}
C++20以降
正にタイプパニングを行うstd::bit_cast
が加わりました.
#include <cstdint>
#include <bit>
#include <iostream>
int main(void)
{
int32_t x=0;
float f = std::bit_cast<float>(x);
std::cout << f << std::endl;
return 0;
}
なんでも型
C++17からより安全な共用体として, std::variant
とstd::any
が追加されました. 誤った使い方をした場合例外を投げてくれます.
#include <cstdint>
#include <iostream>
#include <variant>
#include <any>
int main(void)
{
using namespace std;
variant<int32_t, float> v = 0;
any a = 0;
cout << get<int32_t>(v) << endl;
cout << any_cast<int32_t>(a) << endl;
cout << get<float>(v) << endl; //bad_variant_access例外
cout << any_cast<float>(a) << endl; //std::bad_any_cast例外
return 0;
}
std::variant
はコンパイル時に入れることができる型が決まるので, 従来のunion
に近いです. 入れた型を忘れてしまった場合は, std::variant::get_if
やstd::variant::visit
を使用します.
std::any
はランタイムで動的に入れる型を変更することができます. 入れた型を忘れた場合は, std::any::has_value
やstd::any::type
を使用します.
まとめ
簡単な方法がないため, 処理系も仕方なく上手く処理していたようで, 間違った方法が解説されていたように思います. プロダクトでは移植性のある方法に切り替えていくとよいでしょう.