CPP06 1.0
読み取り中…
検索中…
一致する文字列を見つけられません
C++ Module 06 : キャストと型の探求

‍C++ Module 06は、静的キャストから動的キャスト、さらには危険な再解釈キャストまで、C++の多様な「型変換(キャスト)」に焦点を当てています。このモジュールを通じて、コンパイル時と実行時における型の扱い方と、それに伴う強力さ、そして危険性について深く学びます。

この絵についての説明

Exercise 00 : Conversion of scalar types (スカラ型の変換)

課題の目的

‍文字列として与えられたリテラルを、 char , int , float , double の4つの基本的なスカラ型に変換し、その結果を表示する。変換不可能な場合や特殊な値( nan , inf )も正しく扱う。

主要な概念とテクニック

static_cast : コンパイル時に型チェックが行われる、比較的安全なキャスト。主に数値型間の変換や、voidポインタからの変換などに使われる。今回の課題では、基準となる double 型から他の数値型への変換に用いた。

文字列から数値への変換 :

  • std::strtod() : 文字列を double 型に変換する標準ライブラリ関数。変換できなかった部分のポインタを第二引数で受け取れるため、厳密なパースチェックが可能。

特殊な浮動小数点数 :

  • nan (Not a Number), inf (Infinity) といった疑似リテラルを扱う。
  • <cmath> ヘッダの std::isnan(), std::isinf() を使って判定する。

数値の限界値 :

  • <limits> ヘッダの std::numeric_limits を使い、各型の最大値・最小値を取得してオーバーフローをチェックする。

文字の表示判定 :

  • <cctype> ヘッダの std::isprint() を使い、char 型に変換した結果が表示可能な文字であるかを判定する。

コードのポイント

変換処理の基本的な流れ:

  1. 入力が nan , inf などの疑似リテラルか判定する。
  2. std::strtod()double 型に変換してみる。パースが成功したか(文字列の末尾まで読めたか)をチェックする。
  3. double 型の値を基準に、 static_cast を使って char , int , float へ変換する。
  4. 各型へ変換する際に、オーバーフローや表現不可能な値(nanint にするなど)にならないかチェックし、結果を表示する。

注意点・ハマりどころ

‍* std::strtod() の第二引数 endptr のチェックは非常に重要。これが入力文字列の先頭を指したままなら、全く数値として解釈できなかったことを意味する。

  • float 型のリテラルとして表示する際の f の付け忘れに注意。std::fixedstd::setprecision(1) を使うと、小数点以下の表示を揃えやすい。

出力形式の桁数の意義について

‍浮動小数点数における「桁数」は、単に小数点以下の長さではなく、 「有効数字(significant digits)」 、つまり その型がどれだけ正確に数値を表現できるか という精度を表します。

<tt>float</tt> (単精度浮動小数点数)

‍* 精度: 有効数字 約6〜7桁

  • 意義: floatは4バイトのメモリしか使用しないため、軽量です。しかし、その代償として精度には限界があります。例えば、1.23456789f という数値をfloat型の変数に格納しようとしても、 7桁目あたりで情報が丸められ 、正確な値を保持することはできません。
  • 用途: 大量の数値を扱うグラフィックス(頂点座標など)や、それほど高い精度が要求されない計算で、メモリ使用量を節約したい場合に適しています。

<tt>double</tt> (倍精度浮動小数点数)

‍* 精度: 有効数字 約15〜17桁

  • 意義: doubleは8バイトのメモリを使用し、floatの2倍以上の精度を持ちます。これにより、より広範囲の数値を、より正確に表現できます。科学技術計算や金融計算など、わずかな誤差が大きな問題につながる分野では、doubleの使用が標準です。
  • 用途: C++では、特に理由がない限り浮動小数点数にはdoubleを使うのが一般的です。floatよりも精度が高く、意図しない計算誤差を減らすことができます。

Exercise 00 における意義は、"3.141592653589793"のような精度の高いリテラルが与えられたときに、doubleではその値をほぼ保持できるのに対し、floatにキャストすると 情報が失われる(精度が落ちる) という事実を、出力結果を通じて確認することにあります。これは、各データ型が持つ本質的な限界を理解するための重要な演習

Exercise 01:Serialization (シリアライズ)

課題の目的

‍データ構造体へのポインタを、整数型 uintptr_t に変換(シリアライズ)し、その整数から元のポインタへ復元(デシリアライズ)する。

主要な概念とテクニック

‍* reinterpret_cast : 最も危険で強力なキャスト。ある型を、全く無関係な別の型として「再解釈」する。ビットパターンをそのままに型情報だけを書き換えるイメージ。

  • uintptr_t : ポインタの値を欠損なく格納できることが保証されている符号なし整数型。 <stdint.h> または <cstdint> で定義されている。アーキテクチャ(32bit/64bit)に関わらず、ポインタを整数として扱いたい場合に最適。
  • シリアライズ : メモリ上のオブジェクト(データ構造)を、ファイル保存やネットワーク転送が可能な形式(バイト列や文字列など)に変換すること。今回はその最も単純な形として、メモリアドレスを整数に変換した。

コードのポイント

// シリアライズ: Data* -> uintptr_t
uintptr_t serialize(Data* ptr) {
return reinterpret_cast<uintptr_t>(ptr);
}
// デシリアライズ: uintptr_t -> Data*
Data* deserialize(uintptr_t raw) {
return reinterpret_cast<Data*>(raw);
}
A simple data structure for serialization testing.
Definition Data.hpp:29

注意点・ハマりどころ

‍* reinterpret_cast は型の安全性を完全に破壊する。コンパイラは何も保証してくれず、プログラマの自己責任で使う必要がある。

  • 異なる型のポインタ間での reinterpret_cast は、アライメントやオブジェクトのサイズの違いから未定義動作を引き起こす可能性が非常に高い。
  • 今回の課題のように「ポインタ ⇔ 整数」の変換は reinterpret_cast の数少ない正当な用途の一つだが、それでも使用は慎重に行うべき。

Exercise 02: Identify real type (実際の型を識別)

課題の目的

typeinfo を使わずに、基底クラスのポインタまたは参照が指しているオブジェクトの「実際の型(派生クラスの型)」を特定する。

主要な概念とテクニック

‍* dynamic_cast : 実行時に、安全なダウンキャスト(基底クラスから派生クラスへのキャスト)を行うためのキャスト。

  • ポリモーフィズム (多態性) : dynamic_cast が機能するための大前提。基底クラスに少なくとも1つの仮想関数(通常は 仮想デストラクタ )を持つ必要がある。
  • 実行時型情報 (RTTI) : dynamic_cast は、RTTIを利用して実行時の型をチェックする。

コードのポイント

dynamic_cast は、対象がポインタか参照かで挙動が異なる。

1. ポインタの場合:

‍* キャストが成功すれば、派生クラスへの有効なポインタを返す。

  • キャストが失敗すれば、NULL **(ヌルポインタ) を返す** 。
void identify(Base *p) {
std::cout << "identify(pointer): Actual type is ";
if (dynamic_cast<A *>(p))
std::cout << "A";
else if (dynamic_cast<B *>(p))
std::cout << "B";
else if (dynamic_cast<C *>(p))
std::cout << "C";
else
std::cout << "Unknown";
}
An empty class that inherits from Base.
Definition Base.hpp:40
An empty class that inherits from Base.
Definition Base.hpp:46
A base class with a virtual destructor.
Definition Base.hpp:26
An empty class that inherits from Base.
Definition Base.hpp:52
T endl(T... args)
void identify(Base *p)
Identifies the actual type of an object via a pointer.
Definition main.cpp:51

2. 参照の場合:

‍* キャストが成功すれば、派生クラスへの有効な参照を返す。

  • キャストが失敗すれば、 catch (...) {} の処理へつまり他の派生クラスへのキャストを処理する。
void identify(Base &p) {
std::cout << "identify(reference): Actual type is ";
try {
(void)dynamic_cast<A &>(p);
std::cout << "A";
} catch (...) {
try {
(void)dynamic_cast<B &>(p);
std::cout << "B";
} catch (...) {
try {
(void)dynamic_cast<C &>(p);
std::cout << "C";
} catch (...) {
std::cout << "Unknown";
}
}
}
}
覚え書き
  • 上記の手法が通常行われるが、 std::bad_cast のエラーを使うためには <typeinfo> をヘッダーにインクルードする必要がある。
  • ただし、課題要件として、 <typeinfo> をヘッダーにインクルードすることは禁止されているため参考までに。
    // 禁止事項「<typeinfo>をインクルード」すると、std::bad_castが使えるようになる。
    #include <typeinfo>
    void identify(Base &p) {
    std::cout << "identify(reference): Actual type is ";
    try {
    (void)dynamic_cast<A &>(p);
    std::cout << "A";
    } catch (const std::bad_cast &e) {
    try {
    (void)dynamic_cast<B &>(p);
    std::cout << "B";
    } catch (const std::bad_cast &e) {
    try {
    (void)dynamic_cast<C &>(p);
    std::cout << "C";
    } catch (const std::bad_cast &e) {
    std::cout << "Unknown";
    }
    }
    }
    }

注意点・ハマりどころ

‍* 仮想デストラクタの重要性 : 基底クラスに仮想デストラクタ(または他の仮想関数)がないと、dynamic_cast はコンパイルエラーになる。ポリモーフィックなクラス階層でないと RTTI が有効にならないため。

  • 参照の dynamic_casttry-catch ブロックで囲む必要があることを忘れないようにする。

Project Links

‍* Intra - CPP06

  • GitHub - CPP06 (非公開)
  • My Documents : CPP06 / CPP_Modules

    ‍Doxygenで作成された学習ノートです。

    学習の振り返りや、レビューでも活用しています。


レビューに向けて

レビューの結果(初回1人目)

  • スコア66点 NG
  • ex00 出力結果において、float/double が指数表記になった時に、'.0f' / '.0' が指数部の数字に続く形で表示されていたことが指摘されました。
  • ex02 評価項目として、try - catch を使うことが求められていたことや、pointer を使ってはいけないことに反していた。

Other

  • ブランチの使い方

    main: 安定版

    feature/ex00: 各課題の機能開発を行う。

    docs: ここにpushすると、GitHub Actions workflowが動作してドキュメントが公開されます。

  • ドキュメントページ

    ‍C++ Moduleのプロジェクト毎にDoxygenを使ったドキュメントをまとめています。

    doxygen用のcssテーマは、Doxygen Awesomeを使用。