C++のtypedefとは
C++のtypedefとは、既存の型に別名を付けるための機能です。
たとえば、次のように記述します。
typedef unsigned int uint;
この場合、uintはunsigned intの別名になります。
そのため、次の2つは同じ意味です。
uint age = 20;
unsigned int age = 20;
typedefは新しい独立した型を作る機能ではありません。
あくまで既存の型に別の名前を付ける仕組みです。
typedefの基本的な書き方
typedefの基本構文
typedefの基本構文は次のとおりです。
typedef 元の型 新しい型名;
たとえば、次のように記述できます。
typedef int Integer;
これでIntegerをintの別名として使用できます。
Integer number = 100;
これは次のコードと同じ意味です。
int number = 100;
typedefで定義した名前は通常の型として使える
typedefで作った名前は、その後通常の型名と同じように使用できます。
#include <iostream>
typedef int Integer;
int main()
{
Integer x = 10;
Integer y = 20;
std::cout << x + y << '\n';
return 0;
}
実行結果は次のとおりです。
30
typedefを使うメリット
複雑な型を短く書ける
typedefは、長く複雑な型を何度も使用するときに便利です。
たとえば、次の型を何度も書くとコードが読みにくくなります。
std::vector<std::pair<std::string, int>>
そこで、次のように別名を付けます。
typedef std::vector<std::pair<std::string, int>> UserList;
すると、以降は次のように簡潔に記述できます。
UserList users;
型の意味を分かりやすくできる
同じintであっても、用途に応じて名前を付けることでコードの意味を分かりやすくできます。
typedef int UserId;
typedef int ProductId;
使用例は次のとおりです。
UserId userId = 100;
ProductId productId = 200;
単純にintと書くより、それぞれの値が何を表しているのか理解しやすくなります。
ただし、UserIdとProductIdはどちらも実体はintです。
コンパイラ上では別々の独立した型として区別されません。
typedefは新しい型を作るわけではない
異なるtypedef名でも元の型が同じなら同じ型
次の例を見てみましょう。
typedef int UserId;
typedef int ProductId;
UserId userId = 10;
ProductId productId = 20;
userId = productId;
このコードは問題なくコンパイルできます。
UserIdとProductIdは名前こそ違いますが、どちらもintだからです。
本当に別の型にしたい場合
型安全性を高め、本当に別の型として扱いたい場合は、クラスや構造体などを定義します。
struct UserId
{
int value;
};
struct ProductId
{
int value;
};
この場合、UserIdとProductIdは異なる型としてコンパイラに認識されます。
typedefで整数型に別名を付ける
整数型のtypedef
次のように整数型へ別名を付けられます。
typedef unsigned long ulong;
これにより、
ulong value = 1000;
と書けます。
これは次のコードと同じです。
unsigned long value = 1000;
IDなどの意味を持たせる
たとえばユーザーIDをunsigned longで管理する場合、次のように書けます。
typedef unsigned long UserId;
使用例は次のとおりです。
UserId id1 = 100;
UserId id2 = 200;
このように、元の型を直接書くよりも役割を明確にできます。
typedefで構造体に別名を付ける
C++ではstruct名をそのまま型名として使える
C++では次のように構造体を定義すると、
struct User
{
int id;
std::string name;
};
そのままUserを型名として使用できます。
User user;
そのため、C++では通常、構造体にtypedefを付ける必要はありません。
C言語でよく見られるtypedef struct
次のような書き方を見ることがあります。
typedef struct
{
int id;
std::string name;
} User;
この形式は特にC言語でよく使用されます。
C++では、
struct User
{
int id;
std::string name;
};
と書くだけで十分なため、通常はこちらのほうが自然です。
typedefでポインタ型に別名を付ける
ポインタ型の基本例
ポインタ型にもtypedefを使用できます。
typedef int* IntPtr;
すると、
IntPtr ptr;
は、
int* ptr;
と同じ意味になります。
複数のポインタ変数を宣言するときの違い
通常の宣言で、
int* p1, p2;
と書いた場合、
p1はint*ですが、p2はintです。
一方、次のようにtypedefを使うと、
typedef int* IntPtr;
IntPtr p1, p2;
p1とp2の両方がint*になります。
IntPtrそのものがint*という1つの型の別名だからです。
typedefとconstを組み合わせる場合の注意点
const IntPtrはconst int*ではない
次のコードを見てみましょう。
typedef int* IntPtr;
const IntPtr ptr = nullptr;
一見すると、
const int* ptr;
と同じように見えるかもしれません。
しかし、実際には違います。
IntPtrはint*そのものの別名なので、
const IntPtr ptr = nullptr;
は、
int* const ptr = nullptr;
に相当します。
つまり、ポインタが指しているintではなく、ポインタそのものがconstになります。
const int*との違い
次の宣言は、
const int* ptr;
「変更できないintを指すポインタ」です。
一方、
typedef int* IntPtr;
const IntPtr ptr = nullptr;
は「変更できないポインタ」です。
ポインタ型にtypedefを使用する場合は、この違いに注意が必要です。
typedefで配列型に別名を付ける
配列型のtypedef
配列型にも別名を付けられます。
typedef int IntArray[5];
すると、
IntArray numbers;
は、
int numbers[5];
と同じ意味になります。
配列型を使った例
#include <iostream>
typedef int IntArray[5];
int main()
{
IntArray numbers = {1, 2, 3, 4, 5};
for (int value : numbers)
{
std::cout << value << '\n';
}
return 0;
}
このように、配列のサイズまで含めた型に名前を付けることができます。
typedefで参照型に別名を付ける
参照型もtypedefできる
typedefは参照型にも使用できます。
typedef int& IntReference;
使用例は次のとおりです。
int value = 10;
IntReference ref = value;
ref = 20;
この場合、refはvalueへの参照です。
したがって、refを変更するとvalueも変更されます。
typedefで関数ポインタを分かりやすくする
関数ポインタは構文が複雑になりやすい
関数ポインタは次のように記述します。
int (*func)(int, int);
これは、
「intを2つ受け取り、intを返す関数へのポインタ」
という意味です。
型の構文が少し複雑なので、typedefを使用すると読みやすくできます。
関数ポインタ型をtypedefする
次のように定義します。
typedef int (*Operation)(int, int);
すると、Operationが関数ポインタ型の別名になります。
int add(int a, int b)
{
return a + b;
}
int main()
{
Operation op = add;
int result = op(10, 20);
}
複雑な関数ポインタの宣言を隠せるため、typedefの代表的な用途の一つです。
typedefをクラス内で使用する
クラス専用の型名を定義できる
typedefはクラスの中でも使用できます。
class User
{
public:
typedef unsigned long IdType;
private:
IdType id;
};
クラスの外からは次のように参照できます。
User::IdType id = 100;
このように、クラスやライブラリ内部で意味のある型名を定義できます。
STLにも同様の型名がある
C++標準ライブラリのコンテナには、次のような型名があります。
value_type
size_type
iterator
const_iterator
たとえば、
std::vector<int>::value_type
は、この場合intを表します。
また、
std::vector<int>::size_type
は、std::vectorのサイズを表すために使用される符号なし整数型です。
テンプレート内でtypedefを使う
依存型ではtypenameが必要になる場合がある
テンプレートでは、次のようなコードを書くことがあります。
template <typename T>
void printFirst(const T& container)
{
typedef typename T::value_type ValueType;
ValueType value = container[0];
}
T::value_typeはテンプレート引数Tに依存しているため、それが型であることを示すためにtypenameが必要です。
このtypenameはtypedef固有の機能ではありません。
たとえば、usingでも次のようになります。
using ValueType = typename T::value_type;
typedefでは複数の型名を同時に定義できる
1つのtypedef宣言で複数定義する例
次のように書くこともできます。
typedef int Integer, *IntegerPtr;
この場合、
Integer
はint、
IntegerPtr
はint*です。
ただし、複雑になると読みづらくなりやすいため、可読性を重視するなら個別に定義する方法もあります。
typedef int Integer;
typedef int* IntegerPtr;
typedefとusingの違い
C++11以降ではusingも利用できる
C++11からは、型の別名を定義するためにusingも使用できます。
typedefでは、
typedef unsigned int uint;
と書きます。
usingでは、
using uint = unsigned int;
と書きます。
どちらもuintをunsigned intの別名として定義しています。
usingは複雑な型でも読みやすい場合がある
関数ポインタを比較してみましょう。
typedefの場合は次のとおりです。
typedef int (*Operation)(int, int);
usingの場合は次のようになります。
using Operation = int (*)(int, int);
usingは、
using 新しい型名 = 元の型;
という形式なので、複雑な型では型エイリアスの関係を理解しやすい場合があります。
これは言語仕様上usingのほうが優れているという意味ではありませんが、C++11以降の新規コードではusingが好まれることも多くあります。
usingではエイリアステンプレートを定義できる
テンプレート型に別名を付けられる
usingには、typedefにはない重要な機能としてエイリアステンプレートがあります。
template <typename T>
using Vector = std::vector<T>;
すると、
Vector<int> numbers;
Vector<std::string> names;
と書けます。
それぞれ次と同じ意味です。
std::vector<int> numbers;
std::vector<std::string> names;
typedefそのものをテンプレート化して、このようなエイリアステンプレートを直接定義することはできません。
そのため、テンプレートを活用する現代のC++ではusingが便利です。
typedefとusingはどちらを使うべきか
古いC++との互換性が必要ならtypedef
typedefはC++98などの古いC++から使用できます。
そのため、古いコンパイラや古いコードベースとの互換性が必要な場合にはtypedefが使われることがあります。
C++11以降の新規コードではusingも有力
C++11以降で新しくコードを書く場合は、
using UserId = unsigned long;
のようなusingが使われることも多くあります。
理由としては、
- 宣言形式が分かりやすい
- 複雑な型でも読みやすい場合がある
- エイリアステンプレートが使える
といった点があります。
ただし、typedefが非推奨になったわけではありません。
現在のC++でも正式に使用できる機能です。
typedefとusingの比較
| 項目 | typedef | using |
|---|---|---|
| 型の別名を作る | 可能 | 可能 |
| C++98で使用 | 可能 | 不可 |
| C++11以降で使用 | 可能 | 可能 |
| ポインタ型の別名 | 可能 | 可能 |
| 関数ポインタ型の別名 | 可能 | 可能 |
| 配列型の別名 | 可能 | 可能 |
| エイリアステンプレート | 不可 | 可能 |
| 新規コード | 使用可能 | よく使われる |
固定幅整数型が必要ならcstdintも検討する
独自のtypedefより標準型が適している場合がある
「32ビット整数」を表現したい場合に、
typedef int int32;
のような独自名を作るより、標準ライブラリの<cstdint>を利用する方法があります。
#include <cstdint>
std::int32_t number = 100;
std::int32_tは、その型が実装によって提供されている場合、正確に32ビットの符号付き整数型を表します。
単にintが32ビットであることを前提にするより、意図を明確にできます。
typedefを使うときの注意点
別名を増やしすぎると逆に分かりにくくなる
たとえば、
typedef int A;
typedef A B;
typedef B C;
のように何段階も別名を作ると、最終的に元の型が何なのか分かりにくくなります。
typedefは単に型名を短くするためだけではなく、コードの意味を分かりやすくするために使用することが重要です。
typedefだけでは型安全性は高くならない
次のように、
typedef int UserId;
typedef int ProductId;
としても、UserIdとProductIdは同じintです。
異なる種類の値をコンパイラに厳密に区別させたい場合には、独立したクラスや構造体を使う必要があります。
typedefの使用例
複数のtypedefをまとめて使う例
#include <iostream>
#include <string>
#include <vector>
typedef unsigned long UserId;
typedef std::vector<int> IntVector;
typedef int (*Operation)(int, int);
int add(int a, int b)
{
return a + b;
}
int main()
{
UserId id = 100;
IntVector numbers = {10, 20, 30};
Operation operation = add;
std::cout << "ID: " << id << '\n';
std::cout << "First: " << numbers[0] << '\n';
std::cout << "Result: " << operation(3, 4) << '\n';
return 0;
}
このコードでは、
UserId
がunsigned long、
IntVector
がstd::vector<int>、
Operation
が関数ポインタ型の別名です。
複雑な型や役割のある型に意味の分かりやすい名前を付けることで、コードの可読性を高められます。
C++のtypedefについてのまとめ
C++のtypedefは、既存の型に別名を付けるための機能です。
基本構文は次のとおりです。
typedef 元の型 新しい型名;
たとえば、
typedef unsigned int UserId;
と定義すれば、
UserId id = 100;
のように使用できます。
typedefは整数型だけでなく、ポインタ、参照、配列、関数ポインタなど、さまざまな型に使用できます。
一方で、typedefは新しい独立した型を作るものではなく、元の型に別名を付けるだけです。
また、C++11以降では、
using UserId = unsigned int;
というusingによる型エイリアスも利用できます。
typedefは現在でも有効なC++の機能ですが、新しくC++11以降のコードを書く場合には、可読性やエイリアステンプレートへの対応を考慮してusingを選択することも有力です。
以上、C++のtypedefの意味や使い方についてでした。
最後までお読みいただき、ありがとうございました。
