C++は、プログラミング言語の中でも特に静的型付けの性質が強く、その型システムを正しく理解することは、効率的で安全なコードを書くための第一歩です。

C++の型システムは、C言語から継承した基本的なデータ型から、現代的なオブジェクト指向、テンプレート、そして近年のC++11/14/17/20/23といった標準化によって導入された高度な型推論や型安全なコンテナまで、非常に多岐にわたります。

本記事では、C++で利用されるあらゆる「型」について、その基礎から応用、そして最新の仕様までを網羅的に解説します。

初心者の方には体系的な知識の整理として、中上級者の方には最新仕様の再確認として役立つ内容となっています。

C++の型システムの基本概念

C++における「型」とは、データがメモリ上でどのように表現され、そのデータに対してどのような操作が可能であるかを定義するものです。

C++は静的型付け言語であるため、すべての変数や式の型はコンパイル時に決定されている必要があります。

これにより、実行時のオーバーヘッドを最小限に抑えつつ、型によるエラーチェックをコンパイル段階で行うことができます。

C++の型は大きく分けて以下のカテゴリーに分類されます。

  1. 基本型(Fundamental Types):整数、浮動小数点数、文字、真偽値など、言語に組み込まれた型。
  2. 複合型(Compound Types):配列、ポインタ、参照、関数など。
  3. ユーザー定義型(User-Defined Types):クラス、構造体、共用体、列挙型。
  4. 標準ライブラリ型std::stringstd::vectorなど、標準ライブラリで提供される型。

これらの型を適切に使い分けることで、メモリ効率とプログラムの可読性を両立させることが可能になります。

基本データ型(組み込み型)

基本データ型は、C++言語そのものが提供する最も単純な型です。

これらは算術型(整数型と浮動小数点型)およびvoid型、std::nullptr_t型などで構成されます。

整数型

整数を扱うための型です。

符号あり(signed)と符号なし(unsigned)の2種類があります。

デフォルトではsignedとして扱われます。

型名意味一般的なサイズ(ビット)
char文字、または小さな整数8
short短精度整数16
int標準的な整数16以上(通常32)
long長精度整数32以上
long long超長精度整数64

C++において、これらの型のサイズは環境(アーキテクチャやコンパイラ)に依存することに注意が必要です。

例えば、int型は16ビット以上のサイズであることが保証されていますが、現代の多くのシステムでは32ビットです。

浮動小数点型

実数を扱うための型です。

精度に応じて以下の3種類が定義されています。

型名意味精度
float単精度浮動小数点数
double倍精度浮動小数点数
long double拡張精度浮動小数点数

通常、数値計算ではdoubleが標準的に使用されます。

メモリを節約したい場合やGPUプログラミングなどではfloatが選ばれることもあります。

その他の基本型

  • 真偽値型(booltrueまたはfalseのいずれかの値を持ちます。
  • ボイド型(void:値を持たないことを示す特殊な型です。関数の戻り値がない場合や、汎用ポインタ(void*)として利用されます。
  • 文字型(wchar_t, char16_t, char32_t, char8_t:多言語対応やUnicode文字を扱うための型です。char8_tはC++20で導入されました。

基本型の使用例

C++
#include <iostream>

int main() {
    // 整数型の宣言
    int age = 25;
    unsigned int distance = 1000u;
    long long large_value = 123456789012345LL;

    // 浮動小数点型の宣言
    float weight = 65.5f;
    double pi = 3.141592653589793;

    // 真偽値
    bool is_active = true;

    // 文字型
    char grade = 'A';

    std::cout << "Age: " << age << ", Pi: " << pi << ", Active: " << is_active << std::endl;

    return 0;
}
実行結果
Age: 25, Pi: 3.14159, Active: 1

固定幅整数型(<cstdint>)

前述の通り、intlongのサイズは環境依存です。

しかし、ネットワーク通信やバイナリファイルの読み書きなどでは、厳密にビット数が決まっている型が必要になります。

そこで導入されたのが<cstdint>ヘッダーで定義される固定幅整数型です。

主要な固定幅整数型

  • int8_t / uint8_t:8ビット(符号あり/なし)
  • int16_t / uint16_t:16ビット
  • int32_t / uint32_t:32ビット
  • int64_t / uint64_t:64ビット

これらを使用することで、プラットフォームが変わってもデータサイズが変わらないことが保証され、移植性の高いプログラムを記述できます。

複合型:配列・ポインタ・参照

基本型を組み合わせて作られるのが複合型です。

これらはメモリ上のデータの配置やアクセス方法を定義します。

配列(Arrays)

同じ型の要素をメモリ上に連続して配置したものです。

C言語スタイルの配列は現在も利用可能ですが、サイズ固定であることや境界チェックがないことに注意が必要です。

C++
int numbers[5] = {1, 2, 3, 4, 5}; // Cスタイル配列

ポインタ(Pointers)

変数のメモリ上のアドレスを格納する型です。

C++においてポインタは非常に強力ですが、メモリリークや不正アクセス(ダングリングポインタ)の原因にもなりやすいため、注意深い管理が求められます。

C++
int value = 10;
int* ptr = &value; // valueのアドレスを指すポインタ
std::cout << *ptr << std::endl; // 間接参照して値(10)を表示

参照(References)

既存の変数に対する「別名」として機能します。

ポインタと似ていますが、一度初期化すると指し示す対象を変更できないnullptrを許容しないといった特徴があり、ポインタよりも安全に使用できます。

また、C++11からは「右辺値参照(R-value reference)」が導入され、ムーブセマンティクスによる効率的なリソース移動が可能になりました。

  • 左辺値参照(T&:通常の名前付き変数への参照。
  • 右辺値参照(T&&:一時オブジェクト(右辺値)への参照。
C++
int x = 10;
int& ref = x;  // xに「ref」という別名を付ける
ref = 20;      // refを書き換えると、元のxも20になる
std::cout << x; // 実行結果: 20

int&& rref = 10 + 20; // 計算結果などの「一時的な値」を保持できる
rref += 5;            // その値をそのまま再利用(ムーブ)できる
std::cout << rref;    // 実行結果: 35

ユーザー定義型

C++の強力なオブジェクト指向機能やデータ構造の定義を支えるのがユーザー定義型です。

構造体(struct)とクラス(class)

データをカプセル化し、それに対する操作(メソッド)をまとめるための仕組みです。

C++において、structclassの違いはデフォルトのアクセス権限のみです。

  • struct:デフォルトがpublic
  • class:デフォルトがprivate

現代のC++では、純粋なデータ保持用(POD: Plain Old Data)にはstruct、カプセル化や継承を伴うロジックを持つものにはclassを使い分けるのが一般的です。

C++
#include <iostream>

class BankAccount {
    // ここはデフォルトで private になるため、外から「balance」は触れない
    double balance;

public:
    // コンストラクタ(初期化)
    BankAccount(double initial_balance) {
        balance = initial_balance;
    }

    // 預金する関数(ルールに基づいた操作)
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << amount << "円入金しました。" << std::endl;
        }
    }

    void showBalance() {
        std::cout << "現在の残高: " << balance << "円" << std::endl;
    }
};

int main() {
    BankAccount myAccount(1000);

    // myAccount.balance = 1000000; // エラー!直接書き換えはできない(安全)
    myAccount.deposit(500);         // 公開されている関数を通して操作する
    myAccount.showBalance();

    return 0;
}
実行結果
太郎さんは20歳です。
500円入金しました。
現在の残高: 1500円

列挙型(enum)

関連する定数群に名前を付けるための型です。

C++11以降は、より型安全なスコープ付き列挙型(enum classの使用が強く推奨されています。

C++
// スコープ付き列挙型(推奨)
enum class Color {
    Red,
    Green,
    Blue
};

Color myColor = Color::Red;

従来のenumは暗黙的に整数へ変換されたり、名前空間を汚染したりする問題がありましたが、enum classはこれらの問題を解決しています。

CV修飾子:constとvolatile

型に対して付加的な意味を与えるのが修飾子です。

const修飾子

その変数が「読み取り専用」であることを示します。

プログラムの意図を明確にし、意図しない書き換えを防ぐために極めて重要です。

「可能な限りconstを付ける」のがC++のベストプラクティス(const正しさ)です。

C++
#include <iostream>
#include <string>

// 引数に const を付けると、関数の中で引数を書き換えられなくなる
void printMessage(const std::string& msg) {
    // msg = "Changed"; // ここで書き換えようとするとコンパイルエラーになる
    std::cout << msg << std::endl;
}

int main() {
    // 1. 変数に const を付ける
    const double PI = 3.14159;
    
    // PI = 3.14; // コンパイルエラー:書き換え不可!

    std::cout << "円周率: " << PI << std::endl;

    // 2. 関数に渡す
    std::string text = "Hello Const";
    printMessage(text);

    return 0;
}
実行結果
円周率: 3.14159
Hello Const

volatile修飾子

コンパイラに対して、その変数が外部要因(ハードウェアのレジスタ、マルチスレッド環境など)によって勝手に書き換わる可能性があることを伝えます。

これにより、コンパイラによる過度な最適化(変数の読み込みの省略など)を抑制します。

C++
#include <iostream>

int main() {
    // volatile がないと、コンパイラは「この変数は中で変わってないから、
    // 何度もメモリを見に行く必要はないな」と判断(最適化)してしまうことがある
    volatile bool isReady = false; 

    std::cout << "準備を待っています..." << std::endl;

    // 本来は別のハードウェアや割り込み処理によって isReady が true に書き換わる
    // volatile があれば、毎回必ず「本当の今の値」をメモリまで見に行く
    while (!isReady) {
        // 実際にはここで外部からの変化を待つ
        break; // サンプルを終わらせるための記述
    }

    std::cout << "準備完了!" << std::endl;
    return 0;
}
実行結果
準備を待っています...
準備完了!

最新の型推論機能

C++11以降、型の記述を簡略化し、柔軟性を高めるために強力な型推論が導入されました。

autoによる型推論

初期化子の型から、変数の型をコンパイラに自動決定させます。

複雑なイテレータの型などを記述する際に非常に便利です。

C++
auto x = 10; // xはint型
auto pi = 3.14; // piはdouble型

std::vector<int> vec = {1, 2, 3};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    // 複雑な型名を省略できる
}

decltype

式(変数や関数呼び出しなど)の型をそのまま取得するためのキーワードです。

テンプレートプログラミングなどで、ある式の型と同じ型を定義したい場合に使用します。

C++
int a = 5;
decltype(a) b = 10; // bはaと同じint型

decltype(auto) (C++14)

autodecltypeを組み合わせたもので、関数の戻り値などの型を「参照性を含めて」正確に推論させたい場合に使用します。

C++
int g_value = 42;

int& getRef() { return g_value; } // 参照を返す関数

int main() {
    auto a = getRef();         // autoは参照を消すため、int型(コピー)になる
    decltype(auto) b = getRef(); // 参照を維持するため、int&型になる

    b = 100; // bを書き換えるとg_valueも変わるが、aを変えても変わらない
    return 0;
}

C++標準ライブラリの重要な型

基本型だけでは不十分な、より高度な操作のために標準ライブラリ(STL)が提供する型があります。

これらは現代のC++開発において必須の知識です。

std::stringとstd::string_view

C++ではchar*の代わりにstd::stringを使用するのが標準です。

また、C++17で導入されたstd::string_viewは、文字列の所有権を持たず、メモリコピーなしで部分文字列を参照するための非常に軽量な型です。

C++
#include <iostream>
#include <string>
#include <string_view>

// string_viewを使うと、コピーなしで文字列を受け取れる
void printSimple(std::string_view sv) {
    std::cout << "表示: " << sv << " (長さ: " << sv.length() << ")" << std::endl;
}

int main() {
    std::string full_text = "C++ is powerful and fast.";

    // 文字列の「一部」をコピーせずに参照する
    // 7番目の文字から、8文字分だけを表示
    std::string_view sub = std::string_view(full_text).substr(7, 8);

    printSimple(full_text); // 全体を表示
    printSimple(sub);       // 部分文字列を表示

    return 0;
}

std::optional (C++17)

「値があるかもしれないし、ないかもしれない」という状態を安全に表現するための型です。

ポインタを使ってnullptrで表現するよりも、意図が明確になり型安全です。

C++
#include <optional>
#include <string>

std::optional<std::string> get_name(int id) {
    if (id == 1) return "Alice";
    return std::nullopt; // 値がないことを示す
}

std::variantとstd::any (C++17)

  • std::variant:型安全な共用体(Union)です。指定された複数の型のうち、いずれか一つの値を保持できます。
  • std::any:任意の型の値を保持できるコンテナです。
C++
#include <iostream>
#include <variant>
#include <any>
#include <string>

int main() {
    // --- std::variant (型安全な共用体) ---
    // intかstringのどちらか一つを保持できる
    std::variant<int, std::string> v = 10;
    std::cout << "variantの値: " << std::get<int>(v) << std::endl;

    v = "Hello"; // 途中で型を変えてもOK
    std::cout << "variantの値: " << std::get<std::string>(v) << std::endl;

    // --- std::any (任意の型) ---
    std::any a = 3.14; // doubleを入れる
    std::cout << "anyの値: " << std::any_cast<double>(a) << std::endl;

    a = true; // boolを入れ直す
    if (std::any_cast<bool>(a)) {
        std::cout << "anyの中身は true です" << std::endl;
    }

    return 0;
}
実行結果
variantの値: 10
variantの値: Hello
anyの値: 3.14
anyの中身は true です

std::span (C++20)

連続したメモリ(配列やstd::vectorなど)の「一部」を指し示すための型です。

ポインタとサイズのペアを安全かつ抽象的に扱うことができます。

C++
#include <iostream>
#include <vector>
#include <span>

// std::span を使うと、配列でもvectorでも同じように受け取れる
void printData(std::span<int> data) {
    std::cout << "データ一覧: ";
    for (int x : data) {
        std::cout << x << " ";
    }
    std::cout << "(要素数: " << data.size() << ")" << std::endl;
}

int main() {
    int arr[] = {1, 2, 3};           // C言語スタイルの配列
    std::vector<int> vec = {10, 20, 30, 40}; // vector

    printData(arr); // 配列を渡す
    printData(vec); // vectorを渡す

    // vectorの「真ん中の2つだけ」を切り出して渡す(コピーは起きない)
    std::span<int> sub_span = std::span(vec).subspan(1, 2);
    std::cout << "部分的な";
    printData(sub_span);

    return 0;
}
実行結果
データ一覧: 1 2 3 (要素数: 3)
データ一覧: 10 20 30 40 (要素数: 4)
部分的なデータ一覧: 20 30 (要素数: 2)

型変換とキャスト

C++は型に厳格ですが、異なる型同士の変換が必要な場面もあります。

Cスタイルのキャスト(int)xは強力すぎて危険なため、C++では以下の名前付きキャストを使用します。

static_cast

最も一般的なキャストです。

数値型の変換(doubleからintなど)や、継承関係にあるポインタ同士の変換に使用します。

コンパイル時にチェックが行われるため、比較的安全です。

C++
#include <iostream>

int main() {
    double pi = 3.14159;

    // double型からint型へ変換(小数点以下が切り捨てられる)
    int i = static_cast<int>(pi);

    std::cout << "元の値: " << pi << std::endl;
    std::cout << "変換後: " << i << std::endl;

    return 0;
}
実行結果
元の値: 3.14159
変換後: 3

dynamic_cast

多態的(ポリモーフィック)なクラス階層において、ダウンキャストを行うために使用します。

実行時に型チェックを行い、変換不可能な場合はポインタならnullptrを返します。

C++
#include <iostream>

class Animal { public: virtual ~Animal() {} }; // 親クラス
class Dog : public Animal { public: void bark() { std::cout << "ワン!" << std::endl; } }; // 子クラス
class Cat : public Animal { }; // 別の子クラス

int main() {
    Animal* a = new Dog(); // 中身はDog

    // AnimalポインタをDogポインタに変換してみる
    Dog* d = dynamic_cast<Dog*>(a);

    if (d) {
        std::cout << "変換成功:";
        d->bark();
    } else {
        std::cout << "変換失敗" << std::endl;
    }

    delete a;
    return 0;
}
実行結果
変換成功:ワン!

const_cast

オブジェクトのconst属性を取り除く(または付与する)ためのキャストです。

どうしても変更が必要なレガシーAPIとのインターフェースなどで限定的に使用します。

C++
#include <iostream>

void printMessage(char* str) { // constがついていない古い関数を想定
    std::cout << str << std::endl;
}

int main() {
    const char* text = "Hello World";

    // printMessageはchar*を要求するが、textはconst char*なのでエラーになる
    // そこで const_cast で const を取り除く
    printMessage(const_cast<char*>(text));

    return 0;
}
実行結果
Hello World

reinterpret_cast

ポインタを全く別の型のポインタに変換するなど、ビットレベルでの再解釈を行います。

最も危険なキャストであり、低レベルなプログラミング以外での使用は避けるべきです。

C++
#include <iostream>

int main() {
    int n = 65;
    int* ptr = &n;

    // ポインタ(メモリ番地)を数値(long long型)として読み替える
    long long address = reinterpret_cast<long long>(ptr);

    std::cout << "変数の値: " << n << std::endl;
    std::cout << "変数のメモリ番地: " << address << std::endl;

    return 0;
}
実行結果
変数の値: 65
変数のメモリ番地: 140726831336100 (※実行のたびに変わります)

キャストの使用例

C++
#include <iostream>

class Base { virtual void dummy() {} };
class Derived : public Base {};

int main() {
    double d = 3.9;
    
    // static_cast: 数値変換
    int i = static_cast<int>(d); // i = 3
    
    Base* base_ptr = new Derived();
    // dynamic_cast: 実行時型チェック付き変換
    Derived* der_ptr = dynamic_cast<Derived*>(base_ptr);
    
    if (der_ptr) {
        std::cout << "Successfully cast to Derived" << std::endl;
    }

    delete base_ptr;
    return 0;
}
実行結果
Successfully cast to Derived

C++20/23における型の進化

最新のC++標準化でも、型システムは進化し続けています。

Concepts (C++20)

テンプレート引数に対する「制約」を定義する機能です。

これは厳密には型そのものではありませんが、「どのような性質を持つ型を許容するか」を明示的に記述できるようになり、型安全性が劇的に向上しました。

C++
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

template <Numeric T>
T add(T a, T b) {
    return a + b;
}

このように記述することで、数値型以外をadd関数に渡そうとした場合、難解なテンプレートエラーではなく、分かりやすいコンパイルエラーが出力されます。

std::expected (C++23)

std::optionalを拡張したような型で、「正常な値」または「エラーの理由(エラーオブジェクト)」のいずれかを保持します。

例外(Exception)を使わないエラーハンドリングの標準的な手法として期待されています。

C++
#include <iostream>
#include <expected> // C++23から必要
#include <string>

// 割り算を行う関数
// 成功なら double を、失敗(0除算)なら string のエラーメッセージを返す
std::expected<double, std::string> divide(double a, double b) {
    if (b == 0.0) {
        // エラーのときは std::unexpected で包んで返す
        return std::unexpected("0で割ることはできません");
    }
    return a / b; // 成功ならそのまま値を返す
}

int main() {
    auto result = divide(10.0, 0.0);

    if (result) {
        // result が true なら成功(値が入っている)
        std::cout << "結果: " << result.value() << std::endl;
    } else {
        // result が false なら失敗(エラーが入っている)
        std::cout << "エラー発生: " << result.error() << std::endl;
    }

    return 0;
}
実行結果
エラー発生: 0で割ることはできません

型に関するメタプログラミング

C++では<type_traits>ヘッダーを用いることで、型そのものの性質をプログラム中で調べたり、操作したりすることができます。

  • std::is_integral<T>:Tが整数型か判定。
  • std::is_floating_point<T>:Tが浮動小数点型か判定。
  • std::remove_const<T>:Tからconstを取り除いた型を取得。

これらはテンプレートメタプログラミングにおいて、特定の型に対してのみ処理を切り替える(SFINAEやif constexpr)際に多用されます。

まとめ

C++の型システムは、言語の進化とともに非常に洗練されてきました。

基本となる整数型や浮動小数点型から始まり、std::stringstd::vectorといった標準コンテナ、さらには現代的なstd::optionalstd::variantによる型安全な設計まで、適切に型を選択することは、堅牢なソフトウェア開発の根幹をなします。

特に現代のC++においては、以下の3点を意識することが推奨されます。

  1. 型推論(auto)を適切に使い、コードの冗長さを減らす
  2. 固定幅整数型(int32_t等)を使用し、プラットフォーム依存を避ける
  3. constを積極的に活用し、意図しない状態変化を防ぐ

最新のC++20/23で導入されたコンセプトやstd::expectedなども取り入れ、より高度な型システムを使いこなすことで、C++の持つパフォーマンスと安全性を最大限に引き出していきましょう。