C++のtypedefの意味や使い方について

AI実装検定のご案内

目次

C++のtypedefとは

C++のtypedefとは、既存の型に別名を付けるための機能です。

たとえば、次のように記述します。

typedef unsigned int uint;

この場合、uintunsigned intの別名になります。

そのため、次の2つは同じ意味です。

uint age = 20;
unsigned int age = 20;

typedefは新しい独立した型を作る機能ではありません。

あくまで既存の型に別の名前を付ける仕組みです。

typedefの基本的な書き方

typedefの基本構文

typedefの基本構文は次のとおりです。

typedef 元の型 新しい型名;

たとえば、次のように記述できます。

typedef int Integer;

これでIntegerintの別名として使用できます。

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と書くより、それぞれの値が何を表しているのか理解しやすくなります。

ただし、UserIdProductIdはどちらも実体はintです。

コンパイラ上では別々の独立した型として区別されません。

typedefは新しい型を作るわけではない

異なるtypedef名でも元の型が同じなら同じ型

次の例を見てみましょう。

typedef int UserId;
typedef int ProductId;

UserId userId = 10;
ProductId productId = 20;

userId = productId;

このコードは問題なくコンパイルできます。

UserIdProductIdは名前こそ違いますが、どちらもintだからです。

本当に別の型にしたい場合

型安全性を高め、本当に別の型として扱いたい場合は、クラスや構造体などを定義します。

struct UserId
{
    int value;
};

struct ProductId
{
    int value;
};

この場合、UserIdProductIdは異なる型としてコンパイラに認識されます。

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;

と書いた場合、

p1int*ですが、p2intです。

一方、次のようにtypedefを使うと、

typedef int* IntPtr;

IntPtr p1, p2;

p1p2の両方がint*になります。

IntPtrそのものがint*という1つの型の別名だからです。

typedefとconstを組み合わせる場合の注意点

const IntPtrはconst int*ではない

次のコードを見てみましょう。

typedef int* IntPtr;

const IntPtr ptr = nullptr;

一見すると、

const int* ptr;

と同じように見えるかもしれません。

しかし、実際には違います。

IntPtrint*そのものの別名なので、

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;

この場合、refvalueへの参照です。

したがって、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が必要です。

このtypenametypedef固有の機能ではありません。

たとえば、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;

と書きます。

どちらもuintunsigned 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の比較

項目typedefusing
型の別名を作る可能可能
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;

としても、UserIdProductIdは同じ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の意味や使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次