はじめに
C言語のキャストに関する仕様について、諸々ちょっと調べてみました。
なお、本記事内のソースコードおよびエラーメッセージは以下のgcc(コンパイル時は-Wall -Wextra
を付加)によりコンパイルしたものです。
$ gcc --version
gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
以降の点付き数字で書いてある記述(x.x.x
やx.x.x-x
のようなもの)はC11規格のdraft(N1570)内番号に対応します。
キャスト演算子(cast operator)について
「型変換のうちキャスト演算子を用いて記述されるもの」を指してキャストと呼びます1。すなわち以下のようなものを指します。
int *num = (int *)malloc(sizeof(int));
malloc()
の戻り値はvoid *
型ですが、これをint *
型へと変換している2()
をキャスト演算子(cast operator)と呼びます。本稿ではこのキャスト演算子についての記載をメインにします。
キャスト演算子は(型名)式
の形式になります。キャスト演算子には規格上以下のような規定(6.5.4)となっています。
- 制約(Constraints)
- 型名が
void
型でない場合、修飾された、あるいは修飾されないスカラ型(後述)であり、かつオペランド(キャスト対象の式)もスカラ型でなければならない。 - ポインタを含む型変換は、6.5.16.1に規定されているものを除き、明示的なキャストで指定されなければならない。3
- ポインタ型と浮動小数点数型との間の型変換を行ってはいけない。
- 型名が
- 意味規則(Semantics)
- 丸括弧つきで式の前に指定された型名によって、式の値は指定された型の値へと変換される。この構造をキャスト4と呼ぶ。変換が行われないキャストは、型や式の値に何の影響も及ぼさない。
- 式の値が、キャスト(6.3.1.8)によって指定された型が要求する値よりも大きい範囲あるいは精度である場合、例え式の型が指定した型と同じであっても、そのキャストは型変換を明示し、いかなる超過した分の範囲や精度をも削除する。
要するにどういうこと? という点については後述します。
C言語における型
先ほど出てきた「スカラ型」という言葉には馴染みがないかもしれません。そもそもC言語における型の分類はどの程度の種類があるのでしょうか。どうやら以下のような分類のようです(6.2.5, const
等による修飾型は除く)。
- オブジェクト型(object type)
- 完全型(complete type)
- 基本型(basic type)
- 算術型(arithmetic type)
- 実数型(real type)
- 整数型(integer type)
char
- 符号付き整数型(signed integer type)
- 標準符号付き整数型(standard signed integer type)
signed char
,short int
,int
,long int
,long long int
- 拡張符号付き整数型(extended signed integer type)
処理系定義の整数型を各処理系で作ってよい
- 標準符号付き整数型(standard signed integer type)
- 符号なし整数型(unsigned integer type)
- 標準符号なし整数型(standard unsigned integer type)
_Bool
,unsigned char
,unsigned short int
,unsigned int
,unsigned long int
,unsigned long long int
- 拡張符号なし整数型(extended unsigned integer type)
処理系定義の整数型を各処理系で作ってよい
- 標準符号なし整数型(standard unsigned integer type)
- 列挙型(enumeration type):
enum 型名
- 実数浮動小数点数型(real floating type)
float
,double
,long double
- 整数型(integer type)
- 虚数型(complex type)
float _Complex
,double _Complex
,long double _Complex
- 実数型(real type)
- 算術型(arithmetic type)
- 基本型(basic type)
- 不完全型(imcomplete type)
void
- 配列型(array type)のうち、サイズ不明のもの
- 構造体型(structure type), 共用体型(union type)のうち、サイズ不明のメンバを含むもの
- 派生型(derived type)
- 完全型(complete type)
ひとまず、基本の分類はこのくらいにしておきましょう。ただ、ここに「スカラ型」は存在しません。すなわち、別の分類がいくつかあるわけです。
- 文字型(character type)
char
signed char
unsigned char
- スカラ型(scalar type)
- 算術型(arithmetic type)
- ポインタ型(pointer type)
- アグリゲート型(aggregate type)
- 配列型(array type)
- 構造体型(structure type)
スカラ型はここに現れます。それでは、キャスト演算子が許すキャストはどのようなものなのでしょうか。
キャスト演算子の仕様
すでに記載したキャスト演算子の仕様は以下の通りです。
- 任意の型⇒
void
への変換を認める。 - スカラ型⇔スカラ型の変換を認める。
- ただし、浮動小数点数型⇔ポインタ型は禁止。
すなわち、以下のような感じです。
int main(void)
{
char c = 0;
struct { int mi; char mc; } st = {0};
// 規格適合(ただしwarningが出る場合あり)
int i = (int)c;
long l = (long)i;
double d = (double)l;
void *vp = (void *)i; // warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
long long ll = (long long)d;
i = (int)ll;
c = (char)vp; // warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
(void)st;
// 規格違反
// float f = (float)vp; // error: pointer value used where a floating point value was expected
// vp = (void *)f; // error: cannot convert to a pointer type
// ll = (long long)st; // error: aggregate value used where an integer was expected
return 0;
}
まあ、ここまでは大して問題ないでしょう。規格に適合している中で整数型⇔ポインタ型のキャストなんかは警告されていますが、まぁそもそもこんなことをしなければならない状況もさほど多くはないでしょうから、そうそう拝む機会もないかと思います。
ポインタ型キャストとstrict aliasing rules
キャストについては以上のみかと思いきや、これの他に、一般にstrict aliasing rulesと呼ばれているルールが存在します。これは、以下のようなルールとなっています(6.5-7、拙訳)。
オブジェクトは以下のいずれかの型をもつlvalue9によってのみ、その保存された値にアクセスされなければならない。
・そのオブジェクトの有効型10と互換性のある型
・そのオブジェクトの有効型と互換性のある型の修飾型
・そのオブジェクトの有効型に対応する符号付き、または符号なし型
・そのオブジェクトの有効型に対応する符号付き、または符号なし型の修飾型
・上記の型のいずれかをメンバに含むアグリゲート型、または共用体型(再帰的にそのようなアグリゲート型メンバや共用体型メンバをもつものを含む)
・文字型
ここでいう「アクセス」とは、最初に型を指定されて宣言されたオブジェクトに対して、ある変数を介してその中のデータを読み取る/データに書き込むこと11全般を指します。
aliasingというのは、「別名をつける」という意味ですが、ここでは「ある識別子に対して別の名前の変数からアクセスできるようにすること」、すなわち C言語においては「ポインタ型の変数に既存のオブジェクトのアドレスを代入して、ポインタを介して元の変数に読み書きすること」 と言ってよいでしょう。以下に例を示しますが、これは厳密にはキャスト演算子を使う際の規定ではなくキャスト後のポインタ変数による間接アクセスが規格合致であるかどうかの問題となりますが、ともあれよくキャスト周りでは引っかかる部分です。
int i = 0;
int *pi = &i;
// aliasを用いたオブジェクトへのアクセス
printf("%d\n", *pi);
以上を見たところで、int
変数を例にとり、上記のstrict aliasingを遵守したポインタ型キャストを見てみましょう。間違っていたらご指摘下さい。
#include <stdio.h>
// aliasingによるアクセスを行うマクロ
#define ALIASING_ACCESS(T, obj, fmt) \
do { \
T *palias = (T *)&obj; \
printf(fmt "\n", *palias); \
} while (0)
#define ALIASING_ACCESS_AGGREGATE(T, obj, fmt) \
do { \
T *palias = (T *)&obj; \
printf(fmt "\n", palias->member); \
} while (0)
struct st {int member;};
int main(void)
{
// intで宣言されたオブジェクトを作成
int i = 0;
// strict aliasing遵守
ALIASING_ACCESS(int, i, "%d"); // 有効型
ALIASING_ACCESS(const int, i, "%d"); // 有効型の修飾型
ALIASING_ACCESS(unsigned int, i, "%u"); // 有効型の符号なし型
ALIASING_ACCESS(volatile unsigned int, i, "%u"); // 有効型の符号なし修飾型
ALIASING_ACCESS_AGGREGATE(struct st, i, "%d"); // メンバに互換を含むアグリゲート型
ALIASING_ACCESS(char, i, "%c"); // 文字型
return 0;
}
逆に、このstrict aliasing rulesはある型で指定されたオブジェクトへのアクセス方法を規定しているものでしかないため、いったん汎用ポインタであるvoid *
やあらゆる型の互換型としての文字型へのポインタを介したとしても規格違反となります(もちろんキャスト演算子自体の使い方としては問題ありません)。例えば、以下のコードはコンパイル時は何の警告もなかった12のですが、実行する度に違う値を出力するバイナリを生成しました。
// intで宣言されたオブジェクトを作成
int i = 0;
// いったん汎用ポインタに入れて
void *vp = &i;
// int非互換型に突っ込む
long *lp = vp;
// strict aliasing違反
printf("%ld\n", *lp);
上記のように規格合致でないコードを書いた場合、未定義動作(undefined behavior)を発生させることになります。
3.4.3
1 未定義動作
可搬性のない、あるいは誤ったプログラムの構造や、誤ったデータの使用に際した、それによって本国際標準が何の要求も課さない動作
2 メモ:未定義動作として可能な動作の幅は、状況を完全に無視して予測不能な結果をもたらすものから、(ソースコードの)翻訳やプログラムの実行の間に、環境に特有のドキュメント化された仕方で(診断メッセージの発行を伴うか伴わないかで)ふるまうもの、翻訳や実行を(診断メッセージの発行を伴って)終了させるものまである。
要するに「規格としては何が起こるか関知しないし処理系がどう処理してもいいもの」という恐ろしい奴です。PCを爆発させられても文句は言えません(まあそこまでの大惨事になることはないとは思いますが)。もちろん、意図通りに動いても問題ありません。あるコンパイラでは普通に動いているのに、別のコンパイラでコンパイルしたらおかしなことになるパターンも多いです。上記コードも、int
とlong
のサイズが異なる処理系だったので毎回値が変わるバイナリが出来た訳で、int
とlong
のサイズが等しければ問題なく動くこともありえるでしょう。
C/C++では未定義動作になるパターンが非常に多く、鼻から悪魔が出るなどと言われます。未定義動作を出したら悪魔が召喚される処理系はさすがにないと思いますが、それでも生粋のブラックメタラーでもない限りは悪魔召喚の儀式を行わないように注意しましょう。gccやclangには-fno-strict-aliasing
オプションがあり、積極的な最適化を避けることにより生じるおかしな動きをできるだけ回避することができますがどの程度効果があるんだか分かりません。
キャスト不要な型変換
型変換のうち、明示的に型変換を行う必要のないものもあります。いわゆる暗黙の型変換(implicit conversion)です。参考記事の欄に詳しい解説がありますので、詳細はそちらに任せるとして、ここにそのパターンを列挙します。(規格で確認できた分のみ、多分漏れがあると思うのでご指摘あればお願いします。)
- 整数変換
- 整数拡張(integer promotion)
サイズがint
以下の整数型オブジェクトが単項演算子+
・-
・~
およびシフト演算子<<
・>>
のオペランドとして出てくる場合(6.3.1.1-2)- 元の型の値域が
int
の値域で収まる場合⇒int
- 元の型の値域が
int
では収まらずunsigned int
の値域で収まる場合⇒unsigned int
- 元の型の値域が
- 通常の算術型変換の一部(以下)
- 整数拡張(integer promotion)
- 通常の算術型変換(usual arithmetic conversion)
オペランドを複数とる演算子の内、複数のオペランドが共通の実数型(common real type)を要求する演算子13のオペランドに適用する暗黙変換(6.3.1-8)
以下、対象のオペランドについて、上から順番に優先され適用される。- いずれかが
long double
の場合⇒long double
- いずれかが
double
の場合⇒double
- いずれかが
float
の場合⇒float
- いずれも整数型の場合、いずれのオペランドにも整数拡張(integer promotion)を行った後で以下を適用する。
- 同一の型である場合⇒型変換なし
- 双方が符号付き整数型or双方が符号なし整数型の場合⇒整数変換ランク14が上の型
- 符号なし整数型のランクが符号付整数型より高い場合⇒符号なし整数型
- 符号付き整数型が符号なし整数型の全値域をカバーできる場合⇒符号付き整数型
- いずれでもない場合⇒符号付き整数型をもつオペランドと対応する符号なし整数型
- いずれかが
-
typeの配列型⇒typeへのポインタ型(6.3.2.1-3)
ただし、以下の例外15を除く。-
sizeof
演算子のオペランドである場合 -
_Alignof
演算子のオペランドである場合(C11~) - 単項
&
演算子(アドレス演算子)のオペランドである場合 - 配列初期化に使われる文字列リテラルである場合
-
-
typeを返す関数型⇒typeを返す関数へのポインタ型(6.3.2.1-4)
ただし、以下の例外を除く。-
sizeof
演算子のオペランドである場合 -
_Alignof
演算子のオペランドである場合(C11~) - 単項
&
演算子(アドレス演算子)のオペランドである場合
-
- 規定の引数拡張(default argument promotion, 6.5.2.2-6)
関数の省略記法(func(const char *fmt, ...)
の...
で省略された引数)にあたる箇所の引数は、整数型の場合は整数拡張(integer promotion)を適用し、float
の場合はdouble
に変換する。 - 関数呼び出し時の引数への変数代入変換(6.5.2.2-7)
引数に関数定義・プロトタイプ宣言で指定された型と異なる型のオブジェクトを指定した場合、勝手に関数の引数で指定された型に「代入されたかのように」変換される。 - 等価演算子(equality operator)のポインタ変換(6.5.9-5)
- 片方のオペランドが
NULL
定数であり、もう一方がポインタ型である場合、後のポインタ型に合わせる形で前者の型を変換する。 - 片方のオペランドが
void *
およびその修飾型をもち、もう一方がポインタ型である場合、後者をvoid *
に型変換する。
- 片方のオペランドが
- 単純代入(simple assignment)変換(6.5.16-3, 6.5.16.1-2)
代入式(assignment expression)の返す型はlvalue conversion16の後の左辺のオペランドの型となり、単純代入時には右辺のオペランドは代入式の型に変換される。 -
return
文の変数代入変換(6.8.6.4-3)
return
される式(expression)の型が、その関数の戻り値の型と異なる場合、関数戻り値の型をもつオブジェクトに「代入されたかのように」変換される。
キャスト演算子を使用したい状況
ここまで見て来ましたが、キャスト演算子を「使える」状況は非常に多いとはいえ、キャスト演算子を「使いたい」状況は驚くほど少なそうです。
整数同士の除算結果を浮動小数点数で出す
これは初歩的ですね。プログラミング塾のページとかでも割と見る気がします。
#include <stdio.h>
int main(void)
{
int a = 1;
int b = 2;
double answer[] = {(a / b), ((double)a / b)};
printf("%d / %d = %f\n", a, b, answer[0]);
printf("%d / %d = %f\n", a, b, answer[1]);
}
$ ./a.out
1 / 2 = 0.000000
1 / 2 = 0.500000
演算対象の型自体を浮動小数点数に事前にしておきましょうねという話です。演算子/
は乗除演算子(multiplicative operator)の一種です。
6.5.5-6
整数同士で除算する場合、/
演算子の結果は端数を切り捨てた代数的商となる。……
端数切り捨てが発生しないように片方をキャストすると良いでしょう。初心者講座かな?
符号が異なる整数型の比較
以下のコードを実行すると、直感に反する動作が見れます。
#include <stdio.h>
int main(void)
{
int s = -100;
unsigned int u = 10;
if (s < u) {
puts("s < u");
}
else {
puts("s >= u");
}
if (s < (int)u) {
puts("s < u");
}
else {
puts("s >= u");
}
}
$ gcc -Wall -Wextra test_func.c
test_func.c: In function ‘main’:
test_func.c:7:11: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
if (s < u) {
^
$ ./a.out
s >= u
s < u
わざわざ警告を出してくれているので分かりますが、実行結果は(int)-100 >= (unsigned int)10
になってしまいます。以下のような順番で処理されるためこんな意味不明なことになるのです。
-
関係演算子(relational operator)
<
のオペランドには「通常の算術型変換(usual arithmetic conversion)」が適用される。- 双方が整数型であるため、ルールに従い「整数拡張(integer promotion)」が行われる。ただし、
int
もunsigned int
も整数拡張段階では変換されない。 -
int
とunsigned int
は型のランクが同じであり、かつint
はunsigned int
の値域をすべてカバーできる訳ではないため、符号なし整数型であるunsigned int
に統一される。
- 双方が整数型であるため、ルールに従い「整数拡張(integer promotion)」が行われる。ただし、
-
-100
はunsigned int
の値域外なので、符号なし型の値域内の値に変化する。6.2.5-9
……符号なしのオペランドを含む評価はオーバーフローしない。符号なし整数型によって表現できない評価結果は、その結果の型によって表現できる最大値より1大きい数値により割った剰余になるからである。
つまり、(unsigned int)-100
については、-100 mod (UINT_MAX
+ 1)が今回の結果となります。16bitなら65436
、32bitなら4294967196
となるため、10
より大きいと判定されてしまう訳です。こういう場合は、明示的にキャストして暗黙変換の罠を回避することが有効な手段となります。
malloc()
等の戻り値をキャストする
冒頭のコードを再掲します。
int *num = (int *)malloc(sizeof(int));
このキャストは必要でしょうか。stackoverflowでは賛否両論出ています。英語版WikipediaのCでの動的メモリ割り当てのページには以下のように記載されています。
- 長所
- 短所
- C標準下ではただ冗長。
- キャストを加えることで
malloc()
の関数プロトタイプがあるstdlib.h
のインクルード忘れを隠してしまう。malloc()
のプロトタイプがないとき、C90標準はmalloc()
がint
を返す関数であるとみなすことをCコンパイラに要求する。キャストがなければ、C90はその整数がポインタへ代入されるとき診断情報を要求する。しかし、キャストをつけるとこの診断情報は出ないかもしれず、バグを隠してしまう。(long
とポインタが64bitでint
が32bitの、64bitシステム上のLP64のような、)特定のアーキテクチャやデータモデルにおいて、この間違いは実際に未定義動作(undefined behavior)という結果となる。暗黙的に宣言されたmalloc()
が32bit値を返すにも拘らず、実際の関数定義は64bit値を返すからである。関数呼び出しの仕様やメモリレイアウトによっては、これはスタックを破壊する結果となる。この問題はモダン・コンパイラにおいては比較的に気づかれにくいものとはなっていない。C99は暗黙的な宣言を許していないので、コンパイラはint
を戻り値とみなすとしても診断情報を出さなければならないのである19。 - ポインタの型宣言を変更する場合に、
malloc()
が呼び出されるすべての行とキャストをも変更する必要があるかもしれない20。
上記の長所3を理由に、たとえばCERTでは明示的にキャストを行うことを推奨しています。以下のコードを見てみましょう。見ての通りmalloc()
で確保しているサイズとポインタ型のサイズが違っています。
#include <stdlib.h>
#define MALLOC(type) ((type *)malloc(sizeof(type)))
int main(void)
{
long *lp1 = malloc(sizeof(int));
long *lp2 = (int *)malloc(sizeof(int));
long *lp3 = MALLOC(int);
long *lp[] = {lp1, lp2, lp3};
for (size_t i = 0; i < (sizeof(lp) / sizeof(lp[0])); i++) {
free(lp[i]);
}
}
コンパイル結果は以下の通りです。
$ gcc -Wall -Wextra test_func.c
test_func.c: In function ‘main’:
test_func.c:7:17: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
long *lp2 = (int *)malloc(sizeof(int));
^
test_func.c:3:22: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
#define MALLOC(type) ((type *)malloc(sizeof(type)))
^
test_func.c:8:17: note: in expansion of macro ‘MALLOC’
long *lp3 = MALLOC(int);
^~~~~~
キャストが行われているもののみ警告が表示されるという結果になります。そのため、malloc()
においては戻り値の型を明示し、型チェックをコンパイラに肩代わりさせる目的でキャストを行う人も多いと思います。そういう人から「malloc()
はキャストしろ」と言われてそういうものだとやっている人も多分にいます。
おそらく、これをメリットと思うかどうかによってこの方策をとるかどうかが決まることでしょう。たとえば、以下のような間違いは起こり得そうですが、これについては警告されません。
#include <stdlib.h>
int main(void)
{
long *lp = (long *)malloc(sizeof(int));
free(lp);
}
malloc()
が返すのはあくまで型を持たない記憶域であるため、キャストが間違っていれば型の不一致は起こらず、警告はなくなってしまいます。それはCERTも把握しており、最初のコードでさらっと書いていたMALLOC(type)
マクロは更なる間違いへの対策のために紹介されていました。
ただし、このマクロの導入も意見が分かれると思います。このようなマクロを、一人で開発する際に作っておくのは悪くはありませんが、複数人での開発に「戻り値のあるmalloc()
をマクロ定義したから使ってね!」と複数人に触れ回るのはそれだけでコストになりそうです。人員が頻繁に入れ替わる環境では余計そうなるでしょう。私はキャストする派ですが、そもそも私自身、これだけのためにマクロを作りたいとは思いませんし。
ということで、このキャストを使うかどうか自分で決めることができるのであれば、このあたりを秤にかけて判断するのがよいと思います。コーディング規約で決まっていれば別ですが。
戻り値を無視することを明示するvoid
キャスト
#include <stdlib.h>
int main(void)
{
(void)system("echo \"Hello, world!\"");
}
これも賛否あるかと思います。キャスト演算子は任意の型をvoid
にキャストすることができますので、戻り値のvoid
キャストは文法違反になりません。void
の値にアクセスすることもできないため、strict aliasing rules違反もありえませんね。CERTでも例外項目で推奨していますので、戻り値に興味がないことを明示したい際には使ってみてもいいでしょう。
参考記事
参照規格
-
C draft n1570
実質C11のdraft。正規の規格書は高価なので、こちらが現在最もアクセスしやすい一次文献でしょう。残念ながらJISにはなっていないので日本語訳は存在しません。英語も勉強できて一石二鳥ですね。 -
JIS X 3010
日本語版C99と思ってよいでしょう。JISの規格検索からX3010
で検索すれば見れます。また、このサイトでも閲覧できます。訳に困ったりしたらここを見ていました。大部分についてはC11から変わっていないので、規格書リーディングに大いに参考になるかと思います。
C++のキャスト(名前付きキャスト)
-
キャスト - C++ 入門
C++におけるキャストのざっくりとした説明。C++ではCスタイルのキャストよりも用途限定のstatic_cast
やdynamic_cast
などを使いましょう。 -
型の変換 | Programming Place Plus Modern C++編【言語解説】 第9章
dynamic_cast
を除くC++のキャスト解説。普通に使う分にはここの解説で大体間に合うように思います。詳細を探るのであれば規格に当たるか、cppreferenceの各ページ(static_cast
・dynamic_cast
・const_cast
・reinterpret_cast
)を参照するのがよいでしょう。
暗黙の型変換
-
C言語で暗黙の型変換が発生する16のパターン(+演算子の結果型5パターン)
暗黙の型変換については、パターン網羅かつサンプルが分かりやすいこの記事が詳しいのでおすすめです。猫たちは大変にすばらしい存在である。 -
INT02-C. 整数変換のルールを理解する
CERTによる整数拡張(integer promotion)・整数変換ランク(integer conversion rank)・通常の算術型変換(usual arithmetic conversion)のうち整数型に拘るものの解説です。コード例もあり分かりやすいかと思います。 -
(C言語)Array-to-pointer conversionの例外は3つだけではなかった
配列型⇒ポインタ型の暗黙変換に関する解説です。C89/C90の場合とC11の場合に特有の例外について主に記載されています。
strict aliasing rules関係
-
strict aliasing rules, type punning解説 その1
どちらかと言えばC++寄りの内容が多く見られますが、strict aliasing rulesについての日本語記事としては、かなり詳細であると思います。 -
(翻訳)C/C++のStrict Aliasingを理解する または - どうして#$@##@^%コンパイラは僕がしたい事をさせてくれないの!
こちらもC++にかかる内容はありますが、strict aliasing rulesについて詳細に記載された翻訳記事です。こちらは最適化によって規格違反コードが壊れる例も紹介していて、個人的には読み物的にも面白く感じました。 -
EXP39-C. 適合しない型のポインタを使って変数にアクセスしない
これはJPCERTのページ。かなりざっくりした解説になってはいますが、私のちょっとションボリする翻訳ではないstrict aliasing rulesの訳が読みたい方はどうぞ。不適合コードも併せて見ることができます。
-
規格上では、型変換を一部の演算子のオペランドによって行われる暗黙の型変換(implicit conversion)と、キャスト演算による明示的型変換(explicit conversion)の2種に分けています(6.3-1)。 ↩
-
malloc()
のキャストを行うべきか否かについては人によりけりらしいです。後述。 ↩ -
暗黙の型変換については後から軽く触れます。 ↩
-
この箇所には注がついています。曰く、「キャストの結果はlvalueにならない。そのため、修飾された型へのキャストと、その型の修飾されていないものへのキャストでは、その影響は同じである。」らしいです。でも、
char *
へのキャストとconst char *
へのキャストでは違う気がするのはどうなんだろう……? ↩ -
説明中にelement typeとかいう文言が書いてありました(6.2.5-20)。配列の要素の型のことなんでしょうがそれって区別必要なんですかね? ↩
-
6.2.5-1では「object typeとfunction typeがあるよ」という書きぶりなのですが、その割に6.2.5.20ではfunction typeはderived typeの一部扱いになってしまっています。
どっちやねん。本文では後者の見方で記載しています。 ↩ -
ポインタ型の参照先の型をreferenced typeと呼ぶらしいです(6.2.5-20)。 ↩
-
C11以降の機能、かつオプション機能のためC11規格合致の処理系にすら存在しない可能性があります。 ↩
-
よく左辺値と訳されるlvalueは、6.3.2.1-1で規定されています。「lvalueはオブジェクトを指すことができる(
void
以外のオブジェクト型をもつ)式(expression)である。lvalueが評価される時にオブジェクトを指していない場合、動作は未定義である。オブジェクトが特定の型をもつと言われる場合、その型は、そのオブジェクトを指すのに使われるlvalueによって指示されている。変更可能なlvalue(modifiable lvalue)は配列型・不完全型・const
修飾された型ではなく、(変更可能なlvalueが)構造体型・共用体型である場合は、(再帰的にアグリゲート型・共用型を含むあらゆるメンバ・要素をもつものを含み)const
修飾された型をもつメンバを含まない。」C++に関してはこちらが分かりやすいです。 ↩ -
有効型(effective type)については、6.5-6に規定されています。ざっくりまとめると以下のようになります。
・基本的に有効型は宣言に使った型。
・宣言時の型を伴わないオブジェクト(malloc()
あたりで確保したものなど)は、有効型は最初にアクセスした時の型。
・宣言時の型を伴わないオブジェクトへの最初のアクセスがmemcpy()
やmemmove()
による場合、有効型はコピー元の型。 ↩ -
これについては日本語版スタックオーバーフローで質問させてもらいました。3.1でアクセスの定義が「オブジェクトの値を読むこと、または変更すること」とされているので、値の読み込み・書き込みが発生するまでは未定義動作を踏むことはないと見てよいでしょう。 ↩
-
gccとclang(version 6.0.0-1ubuntu2)で
-Wstrict-aliasing
しても警告されませんでした。静的な解析には限界があるということか……。ちなみに、両者ともvoid
ポインタを介さず、明示的キャストせずにlong *lp = &i;
とした場合は-Wincompatible-pointer-types
に引っかかり警告が出ました。明示的キャストをした場合は警告が出ませんでした。↩-Wstrict-aliasing
とは一体……。 -
例えば、条件演算子
?:
の第2・第3オペランドは同一の型であると措定されます。そのため、int i = 1; unsigned int ui = 2; long l = cond ? i : ui;
などとした場合、i
とui
は同一の型へと変換されて処理されることになります。 ↩ -
整数変換ランク(integer conversion rank)というのは、その名の通り整数が型変換される際の優先度をランクづけしたものであり、暗黙変換時に要求する型が決まっていない場合は基本的にこのランクを元に整数変換が行われます。以下のように(6.3.1.1-1)なっています。
・内部の表現が同じでも符号付き整数型同士で同じランクの型はないこととする。(例:short
とint
が揃って16bitの処理系でも、両者のランクは異なる)
・整数型同士での型のランクは、より精度が低い型のランクより高くなる。(例:short
が16bit、int
が32bitの場合、int
の方がランクが上でなければならない)
・符号付き整数型のランクは次の通り。signed char
<short int
<int
<long int
<long long int
・符号なし整数型のランクは、対応する符号付き整数型に等しい。
・標準整数型のランクは、同じ精度の拡張整数型よりもランクが高い。(例:int
(16bit)と、処理系独自に作成された同精度の整数型i_int
(16bit)があった場合、int
の方がランクが高くなる)
・char
のランクはsigned char
とunsigned char
のランクに等しい。
・_Bool
のランクは、他のいかなる整数型のランクよりも低い。
・列挙型のランクは、対応する整数型と同じランクになる。(補足:6.7.2.2.4によると、列挙型の値域は、その型の列挙定数がすべて表現できる整数型のどれかと等しくなります。その制約内でどれになるかは処理系定義です。)
・同精度の拡張符号付き整数型同士のランクは他の規則に従った上で処理系定義。
・ランクについて、T1
>T2
かつT2
>T3
ならT1
>T3
になる。(ランクの上下がT1
>T2
>T3
>T1
> ... のような構造になることはない。) ↩ -
C89/90まではrvalueは暗黙変換の対象にならないので注意。 ↩
-
lvalue conversionとは、「配列型以外のlvalueが、
sizeof
・_Alignof
・++
・--
演算子のオペランドおよび.
・=
演算子の左オペランドでない場合、lvalueが指すオブジェクトの値をもつrvalueへと変換される。その際の型は、そのlvalueの型の非修飾型(unqualified version)となる。」(6.3.2.1-2)といった変換のことを指します。 ↩ -
C++17のdraftを確認(8.2.10-7)しましたが、「オブジェクトポインタは異なる型のオブジェクトポインタへと明示的に変換できる」と記載されているので、明示キャストが必要なようです。g++でキャストを排除すると
error: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]
というエラーでコンパイルできません。そもそもC++から↩malloc()
を使うのが最終手段というか末期的な気がします。 -
そもそも規格化以前の原初のC言語には
void *
がなかったという話だそうです。今どき気にする必要がある現場がどれだけあるかですね。 ↩ -
実際、gccは普通に「インクルード忘れてるぞ」な警告を出してくれるので、この点はあまり気にしなくていいと思います。 ↩
-
まず一つのポインタに入れ替わり立ち替わり
malloc()
されるコードの保守が割と地獄な気がしないでもないです。今どきはポインタ宣言の時点で即malloc()
が普通だと思うので、短所に挙げるほど手間が増えるかはちょっと疑問です。 ↩