C++において列挙型は、意味のある名前を定数に割り当てることでコードの可読性を飛躍的に向上させる重要な機能です。
特にC++11以降で導入されたscoped enumeration(enum class)は、型安全性を高めるために必須の道具となりました。
しかし、厳格な型システムを持つenum classを使用していると、その値を整数として取り扱いたい場面に頻繁に遭遇します。
例えば、ネットワーク通信のためのバイナリシリアライズや、データベースへの値の格納、あるいは古いC言語スタイルのAPIとの連携などが挙げられます。
このようなケースでは、列挙型が内部的にどのような整数型(基底型)を使用しているかを知る必要があります。
本記事では、2026年現在の最新仕様を反映し、C++11から存在するstd::underlying_typeから、C++23で導入された便利なユーティリティstd::to_underlyingまでを詳しく解説します。
列挙型の基底型とは何か
C++の列挙型には、その値を表現するためにメモリ上で実際に使用される整数型が存在します。
これを「基底型(underlying type)」と呼びます。
従来のCスタイル列挙型(unscoped enum)では、基底型はコンパイラによって自動的に決定されることが多く、プログラマが制御しにくい側面がありました。
一方、C++11以降のenum classでは、以下のように明示的に基底型を指定することが可能です。
// 基底型をuint8_tに明示的に指定した列挙型
enum class Status : uint8_t {
Active = 0,
Inactive = 1,
Pending = 2
};
このように型を固定することで、メモリレイアウトの最適化やバイナリ互換性の維持が可能になります。
しかし、テンプレート関数などで任意の列挙型を受け取る場合、その基底型が何であるかをプログラム的に取得する仕組みが必要になります。
そこで登場するのが、標準ライブラリの<type_traits>ヘッダに含まれるstd::underlying_typeです。
std::underlying_typeの基本と使い方
std::underlying_typeは、テンプレート引数に渡された列挙型の基底型を取得するための型特性(Type Traits)です。
この機能を利用することで、列挙型がintなのか、uint8_tなのか、あるいは他の整数型なのかをコンパイル時に判定できます。
基本的な構文
C++11で導入された当初は、::typeを介して型を取得する形式でした。
#include <iostream>
#include <type_traits>
enum class Color : short { Red, Green, Blue };
int main() {
// std::underlying_type<T>::type で型を取得する
using MyUnderlying = std::underlying_type<Color>::type;
std::cout << "Size of underlying type: " << sizeof(MyUnderlying) << " bytes" << std::endl;
return 0;
}
Size of underlying type: 2 bytes
上記の例では、Colorの基底型として指定したshortが正しく取得されていることがわかります。
std::underlying_type_tによる簡略化
C++14からは、::typeを記述する手間を省くために、エイリアステンプレートであるstd::underlying_type_tが導入されました。
現代的なC++開発では、こちらの記述方法が主流です。
// C++14以降の推奨される書き方
using MyUnderlying = std::underlying_type_t<Color>;
これにより、テンプレートメタプログラミングにおけるコードの視認性が向上しました。
C++23で登場したstd::to_underlyingの衝撃
これまでは、列挙型の値を基底型の数値に変換するために、static_castを使用するのが一般的でした。
Status s = Status::Active;
auto value = static_cast<std::underlying_type_t<Status>>(s);
しかし、この記述は非常に冗長であり、コードの至る所に現れると可読性を損なう原因となります。
この不満を解消するために、C++23では標準ライブラリの<utility>ヘッダにstd::to_underlyingが追加されました。
std::to_underlyingの利点
std::to_underlyingは、列挙型の値をその基底型の値に直接変換してくれる関数です。
内部的には前述したstatic_cast<std::underlying_type_t<T>>と同等の処理を行いますが、関数名が意図を明確に示しているため、非常に直感的です。
#include <utility>
enum class Permission : uint32_t {
Read = 1,
Write = 2,
Execute = 4
};
void process_permission(uint32_t p) { /* ... */ }
int main() {
Permission p = Permission::Write;
// C++23以降の非常にシンプルな記述
auto raw_val = std::to_underlying(p);
process_permission(raw_val);
}
この関数を使用することで、キャストに伴う型名の繰り返しを排除し、DRY(Don’t Repeat Yourself)原則を遵守したクリーンなコードが実現します。
実践的なユースケース
ここからは、実務でどのようにこれらの機能が活用されるかを具体的に見ていきましょう。
1. 列挙型の値を標準出力する
enum classは標準出力にそのまま渡すことができず、コンパイルエラーになります。
このような時、std::to_underlyingを使用するのが最もスマートな解決策です。
#include <iostream>
#include <utility>
enum class LogLevel { Info, Warning, Error };
int main() {
LogLevel level = LogLevel::Warning;
// std::to_underlying を使って数値として出力
std::cout << "Current level: " << std::to_underlying(level) << std::endl;
}
Current level: 1
2. ビットフラグとしての利用
列挙型をビットフラグとして扱う場合、ビット演算を行うたびに整数型へ変換する必要があります。
テンプレート関数を作成する際、std::underlying_type_tを戻り値の型として指定することで、柔軟なビット演算ユーティリティが作成できます。
#include <type_traits>
template <typename T>
auto to_int(T e) -> std::underlying_type_t<T> {
return static_cast<std::underlying_type_t<T>>(e);
}
enum class Flags : uint8_t {
None = 0,
Bit1 = 1 << 0,
Bit2 = 1 << 1
};
// ビット論理和の結果を整数として取得する例
uint8_t combined = to_int(Flags::Bit1) | to_int(Flags::Bit2);
3. C言語向けAPIとのインターフェース
C言語で書かれたライブラリでは、オプションの設定にintやunsigned intが多用されます。
C++側で型安全なenum classを定義しつつ、API呼び出しのタイミングでstd::to_underlyingを使用することで、安全性と互換性を両立できます。
| 手法 | メリット | デメリット |
|---|---|---|
| static_cast | 古くからの標準で移植性が高い | 記述が冗長になりやすい |
| std::to_underlying | 簡潔で意図が明確 | C++23以降の環境が必要 |
C++26に向けた展望とリフレクション
2026年現在のC++開発においては、さらなる進化として「リフレクション(Reflection)」が注目されています。
C++26で導入が期待されているリフレクション機能を使えば、列挙型の名前そのものを文字列として取得したり、要素数をコンパイル時に数え上げたりすることがより容易になります。
しかし、単純に「数値としての基底型の値が必要」というケースにおいては、依然としてstd::to_underlyingが最も軽量で高速な手段であり続けるでしょう。
リフレクションは高度なメタプログラミングのためのツールであり、日常的な整数変換には今回紹介した手法を使い分けるのが正解です。
注意点:非列挙型に対する動作
std::underlying_typeを列挙型以外の型(例えば通常のintやクラス)に対して適用しようとすると、コンパイルエラーが発生します。
これを防ぐためには、C++20で導入されたコンセプト(Concepts)を使用して、型を制限するのが良い方法です。
#include <concepts>
#include <type_traits>
template <typename T>
requires std::is_enum_v<T>
auto get_value(T e) {
return std::to_underlying(e);
}
このようにstd::is_enum_vを制約に加えることで、予期せぬ型が渡された際のエラーメッセージが読みやすくなり、堅牢なライブラリ設計が可能になります。
また、C++23以前の環境でどうしてもstd::to_underlyingと同等の機能を使いたい場合は、以下のような自作の関数テンプレートを用意するのが一般的です。
template <typename T>
constexpr auto my_to_underlying(T e) noexcept {
return static_cast<std::underlying_type_t<T>>(e);
}
このようにラップすることで、将来的にC++23へ移行する際、検索・置換だけで標準機能へ乗り換えることができます。
まとめ
列挙型の基底型を扱う機能は、C++の型安全性を維持しつつ実務的な利便性を確保するために欠かせない要素です。
C++11からC++23、そしてC++26へと進化を続ける中で、基底型の取得方法はより「簡潔」で「安全」な方向へと進化してきました。
std::underlying_typeは、テンプレートメタプログラミングにおいて型を定義する際に強力な力を発揮します。
一方で、std::to_underlyingは、日常的なコーディングにおける冗長なキャストを排除し、コードの意図を明確にするために非常に有効です。
これらの機能を適切に使い分けることで、現代的なC++のポテンシャルを最大限に引き出すことができます。
特に新しいプロジェクトでは、積極的にC++23以降の機能を活用し、読みやすくメンテナンス性の高いコードを目指しましょう。
本記事の内容が、あなたのC++プログラミングにおける列挙型の活用の一助となれば幸いです。
