typedef
typedef(タイプデフ)は、プログラミング言語のCおよびC++におけるキーワード(予約語)である。このキーワードはデータ型に新しい名前(エイリアス、シノニム)をつけるために使用される。プログラマが容易にソースコードを記述・理解できるようにすることが目的である。
使用例
[編集]まずtypedef
を使わない例を示す。
int is_even_number(int x) {
return x % 2 == 0;
}
この関数は入力x
が偶数であれば1を、奇数であれば0を返す。しかし、次のようにint
型の別名として論理型(ブーリアン型)を定義し、関数の戻り値の型の名前として使用することで、戻り値の意味を明確化できる。
typedef int my_boolean_t;
my_boolean_t is_even_number(int x) {
return x % 2 == 0;
}
真偽値 (boolean) は通例0を偽、0以外を真とすることから、関数の仕様がより明確となっている[注釈 1]。サフィックス_t
は、typedefによる型エイリアスであることを分かりやすくするために付けられることが多い(size_tやtime_tなど)。
また、typedefは長大で複雑な型名を単純化するために用いられることもある。
typedef unsigned char byte;
typedef unsigned long long ulonglong;
byte vb = UCHAR_MAX;
ulonglong vull = ULLONG_MAX;
次に、構造体を使った例を示す。
struct my_struct {
int data1;
int data2;
char data3;
};
ここでは、ユーザー定義の型my_struct
が定義されている。my_struct
型の変数を宣言するために、C言語では以下のようにstruct
キーワードが必要である。
struct my_struct a;
my_struct b; /* 構文エラー */
ここで、構造体宣言の後に、以下の行を追加してみる。
typedef struct my_struct my_struct_t;
これにより、my_struct
型の変数を宣言するためには、以下のコードで十分となる。
my_struct_t a;
同じことは、以下のコードでも行うことができる。
typedef struct my_struct {
int data1;
int data2;
char data3;
} my_struct_t;
構造体のタグ名を省略することもできる。この場合、タグ名は処理系によって自動生成される。
typedef struct {
int data1;
int data2;
char data3;
} my_struct_t;
構造体のタグ名と別名を同一にすることもできる。
typedef struct my_struct_t {
int data1;
int data2;
char data3;
} my_struct_t;
以上のtypedef
活用は構造体だけに限らず、共用体union
や列挙型enum
の定義時にも同様に適用可能である。
typedefは、自己参照構造体へのポインタの作成を簡単にすることもできる。以下にコード例を示す。
typedef struct node_tag node_t;
struct node_tag {
node_t *nextptr;
int data;
};
通常、ある型のポインタ型変数を宣言する際、それぞれの変数名の前にアスタリスク*
を記述する必要がある。
node_t *startptr, *endptr, *curptr, *prevptr, errptr, *refptr;
プログラマはerrptr
がポインタ型node_t *
であることを想定しているが、typoによりerrptr
は値型node_t
であると定義されてしまっている。これは微妙な構文エラーを引き起こす。
新たにnode_t *
型を定義することでこのような記述ミスを回避できる。以下のコードがその例である。
typedef node_t *node_ptr_t;
node_ptr_t startptr, endptr, curptr, prevptr, errptr, refptr;
これにより、errptr
を含めて全ての変数がnode_t *
型であることが保証される。
再代入や書き換えが不可能な文字列定数の配列(テーブル)を定義するときも、typedef
を利用することで分かりやすくなる。
#if 0
static const char *const planetNamesTable[] = { "Mercury", "Venus", "Earth", "Mars" };
#else
/* 以下は上記と等価。 */
typedef const char *ConstCharPtr;
static const ConstCharPtr planetNamesTable[] = { "Mercury", "Venus", "Earth", "Mars" };
#endif
関数へのポインタを利用する場合も typedef
を利用することで可読性を向上できる可能性がある。
#include <stdio.h>
/* 関数型のエイリアス */
typedef int binary_operator_t(int, int);
static int add(int a, int b) { return a + b; }
int main(int argc, char *argv[]) {
#if 0
int(*f)(int, int) = add;
#else
/* 以下は上記と同義だが、より簡潔かつ明瞭である。 */
binary_operator_t *f = add;
#endif
printf("add(12, 13) = %d\n", f(12, 13));
}
なお、上記は以下のようにも書ける。
#include <stdio.h>
/* 関数ポインタ型のエイリアス */
typedef int (*binary_operator_ptr_t)(int, int);
static int add(int a, int b) { return a + b; }
int main(int argc, char *argv[]) {
binary_operator_ptr_t f = add;
printf("add(12, 13) = %d\n", f(12, 13));
}
関数型もしくは関数ポインタ型のエイリアスを定義しておくと、特にコールバック関数へのポインタを引数として受け取る関数を定義するときに記述性や可読性が向上する。
配列に対して typedef
を利用することもできる。
#include <stdio.h>
typedef int array_int_2_t[2];
int main(int argc, char *argv[]) {
array_int_2_t a;
a[0] = 12;
a[1] = 13;
printf("a[0] = %d, a[1] = %d\n", a[0], a[1]);
}
なお、以下のようなコードに対する動作は未定義となる。グローバルスコープを持ち、アンダースコア_
で始まる識別子、もしくは_
で始まり、その次が大文字の識別子[注釈 2][1][2]は予約済み識別子であり、それを宣言または定義した場合は動作未定義とされているからである[3][4]。
typedef struct _MyStruct {
...
} MyStruct;
マクロとの比較
[編集]C/C++にはテキストの置換機能としてマクロも備わっているが、型名の置換に使うには問題がある。まず、ポインタ型を正しく扱えない。
#define const_char_ptr_t const char *
const_char_ptr_t s1 = "abc", s2 = "ABC"; // s2はconst char型となり、コンパイルエラーを引き起こす。
typedefであればポインタ型を正しく扱える。
typedef const char *const_char_ptr_t;
const_char_ptr_t s1 = "abc", s2 = "ABC";
また、マクロは乱暴な置換を行なうことから、意図しない置換による原因特定のしにくいコンパイルエラーを引き起こすこともある。
C++
[編集]C言語と異なり、C++において構造体、共用体、列挙型、クラス型の変数を宣言する際は、struct
、union
、enum
、class
キーワードの使用はオプションとなっており、あいまいさがないかぎり省略できる。例えば、
struct my_struct {
int data1;
int data2;
char data3;
};
という定義さえあれば、typedef
エイリアスを明示的に定義したり、struct
キーワードを明示的に使用したりせずとも、
my_struct a;
と宣言できる。
C++ではクラス(あるいは構造体)内部でtypedef
を使用することで、クラス スコープのシノニムを定義することができるため、テンプレートを使用したジェネリックプログラミングやダックタイピングに都合がよい。C++標準ライブラリのSTLの実装では、このテクニックが利用されている。
C++のusingエイリアス宣言
[編集]C++ではtypedef
をC言語同様にエイリアス宣言のために利用できるが、C++11ではusing
キーワードによる文法も追加された。
typedef int MyInt; // intの別名MyIntの宣言
C++11では、上記は以下のようにも書ける。
using MyInt = int; // intの別名MyIntの宣言
なお、typedef
はテンプレート化できないが、C++11ではusing
によるエイリアステンプレートが追加された。
template <typename T> using TStringMap = std::map<std::string, T>;
TStringMap<double> diametersTable = { { "Mercury", 4879 }, { "Venus", 12104 }, { "Earth", 12756 }, { "Mars", 6792 } };
批判と利点
[編集]一部の人々は、typedef
を広範に使用することに反対している。ほとんどの議論は、typedef
は単に変数の実際のデータ型を隠すだけであるという考えに集中する。例えば、Linuxカーネルハッカーであり、ドキュメント作成を行っているグレッグ・クロー=ハートマン(Greg Kroah-Hartman)は、関数プロトタイプ宣言を除いて、typedef
の使用をやめさせようとしている。彼は、typedef
を使用することが、必要以上にコードを混乱させるだけでなく、プログラマが巨大な構造体を単純な型と誤認識して使用してしまうことがあると主張している[5]。
しかし、typedef
を推奨して、広範に使用することに大賛成する人々もいる。特に、C言語を発明したブライアン・カーニハン (Brian W. Kernighan) とデニス・リッチー (Dennis M. Ritchie) はプログラミング言語C(英:The C Programming Language)というC言語の定義書に、typedef
の利用に対するメリットを2つ述べている。まず第一は、ソフトウェアをマルチプラットフォーム展開する際に、ソースコードの移植性(ポータビリティ)を向上させる手段として重要なことである。データ型の改良あるいは変更が必要になるときに、必要な変更はただ1つだけのtypedef
の宣言箇所であり、typedef
シノニムを利用してさえいれば多くの箇所を変更する必要がなくなる[注釈 3]。第二に、データを隠すことに加えて、データのカプセル化も向上させるようになり、複雑な宣言がより理解しやすくなることである。
もともとC言語の規格では、基本型(int
などの組み込み型)のサイズや内部表現を厳密に規定しておらず、処理系依存となっている(ターゲット プラットフォームにとって都合の良いサイズや内部表現に設定してよいことになっている)ため、プラットフォーム間の違いを吸収して同じソースコードを使うためにはtypedef
は必須の技術といえる。C99/C++11規格では、int32_t
などのサイズや内部表現を保証する整数型が<stdint.h>/<cstdint>にて標準化されたが、これらは通例typedef
シノニムを利用して実装される。とはいえ、エイリアスによる型定義は万能ではなく、汎整数拡張などのように組み込み型にまつわる問題は依然として残っている[6]。
Microsoft WindowsにおけるWindows APIの例でいうと、Win16とWin32におけるint
型の違いを吸収できるINT32
型がエイリアス定義されている。また、Win16、Win32、Win64におけるポインタ互換整数型の違いを吸収できるINT_PTR
型がエイリアス定義されている[7]。
他の言語
[編集]Haskell、Miranda、Objective Caml等のような、多くの静的型付けの関数型言語では、C言語でのtypedef
と同じ働きをする、type synonymを定義することができる。Haskellでの例を示す。
type PairOfInts = (Int, Int)
これにより、Int
のペアと同じものをtype synonymのPairOfInts
で定義することができる。
PascalおよびObject Pascalではtype
キーワードを使用することで、typedef
同様に別名を定義することができる。
type
TMyInt = Integer; // 組み込み型Integerに対するシノニム。
C/C++の特徴を取り入れたC#言語では、基本型のサイズは厳密に決められており、またtypedef
は言語機能として存在しないが、代わりにusing
エイリアス ディレクティブ機能が存在する[8]。
using MyString = System.String;
ジェネリクスを利用するときは特に型名が長大になりがちであり、ソースファイル中で何度も出現する場合にエイリアスを定義しておくと記述が楽になり、検索や置換も容易になる。
using MyStringToDoubleDictionary = System.Collections.Generic.Dictionary<string, double>;
このusing
エイリアスは、ディレクティブが記述されたソースファイル内でのみ効果があるが、C# 10ではglobal
修飾子を付けることで、ソースファイルを超えてグローバルなエイリアスを定義できるようになった[9]。
脚注
[編集]注釈
[編集]- ^ C++およびC99以降のC言語では、論理型を独自に定義する代わりに言語標準の
bool
型や_Bool
型を使うほうがよいが、ライブラリによっては互換性あるいは相互運用性のためにあえてint
やunsigned char
などのエイリアスを使うこともある。 - ^ 予約名のルールはC/C++で微妙に異なる。Cでは
__
で始まる識別子も予約される。C++では__
を含む識別子も予約される。 - ^ もちろん、printf/scanf書式など、型に依存する部分は型エイリアス変更後に適切に修正される、もしくは(可変長引数に渡す前に書式に対応した明示的な型変換を記述するなどして)型エイリアス変更の影響を受けないような形で正しく利用される、という前提である。
出典
[編集]- ^ Deep C++, 予約名 - MSDN, Internet Archive
- ^ Identifiers (C++) | Microsoft Docs
- ^ “DCL37-C. 予約済みの識別子を宣言または定義しない”. JPCERT/CC (2015年1月22日). 2015年1月25日閲覧。
- ^ [迷信] 構造体のタグ名は下線で始める | 株式会社きじねこ
- ^ Kroah-Hartman, Greg (2002年7月1日). “Proper Linux Kernel Coding Style”. Linux Journal. 2007年9月23日閲覧。 “Using a typedef only hides the real type of a variable.”
- ^ 第5回 int 型のサイズ | 株式会社きじねこ, Internet Archive
- ^ Windows Data Types (BaseTsd.h) - Win32 apps | Microsoft Docs
- ^ using ディレクティブ - C# リファレンス | Microsoft Docs
- ^ §global 修飾子 : using ディレクティブ - C# リファレンス - C# | Microsoft Learn
関連項目
[編集]外部リンク
[編集]- http://www.cprogramming.com/tutorial/typedef.html - detailed discussion of typedef at CProgramming.com