あなたの知らないC言語
この記事はTNCT23s 3J Advent Calendar 2025 21日目の記事です。
皆さん、C言語書いてますか?
実はこのC言語、知られざる構文がたくさん存在します。
この記事ではC言語の変わった構文を紹介します。
[]のシンタックスシュガー
これは割と有名な話かもしれません。
配列にアクセスするとき、
int a[] = {0, 1, 2};
int index = 1;
a[index];
みたいに書くと思います。
このa[index]というのは*(a + index)と等価であるというのもご存知だと思います。
ではこの構文を逆に使うとどうなるでしょうか?
index[a];
indexのa番目にアクセスしようとしていて明らかに気持ち悪いですが、これは*(index + a)と等価ということになりますよね。さらにこれは*(a + index)と等しいです。
?
驚くべきことに、index[a]でもa[index]と同じ値が得られてしまいました。
Comma operator
forで2変数を扱ったりしたいとき、
for (int i = 0, j = 10; i < 5; i++, j++) {}
こんなふうに書けることをご存知でしょうか。
このi++, j++のカンマって何者でしょう?
実は、このカンマは+や*と同じ演算子の一種で、for文でなくても使うことができます。
カンマ演算子の機能は、「両方の式を評価し、右側オペランドの値を返す」です。
int x = ((0, 1), 2); // (0, 1, 2)でも可
このように書けば、xには2が代入されます。
forのi++, j++とは、「i++とj++を両方評価(計算)し、j++の結果を返す」だったのです。for文の更新式では値を返却することには意味がないので、捨てられているだけです。
これを悪用するとこんな事も出来ちゃいます。
int func(int x) {
return x++, x *= 2, do_stuff(x), x;
}
迫力ですね。
Statement Expression
今度はGNU拡張の話です。
C言語の標準ではないものの、便利なのでGNU Cコンパイラが仕様を拡張して独自に実装した機能になっています。
int x = ({
int tmp = 10;
tmp * tmp;
});
こうやると、({})内に書かれた文が実行され、最後の文の結果が変数に代入されます。この例だと、xには10 * 10 = 100が代入されます。いかついですね。
この中では何をやっても割と自由です。
int func(int a) {
return a + 10;
}
int main() {
int x = ({
int a = 10;
int b = 20;
a * func(b);
});
}
こんなこともできますし、
int main() {
char some_condition = 1;
char x = ({
if (some_condition) {
return 0;
}
'a';
});
}
returnなんかもできます。returnした場合、xの初期化を切り上げて関数ごと終了します。
_Generic
int x = 0;
int a = _Generic(x, int: 1, char: 2, default: 3);
_Genericは、第一引数の型に応じて返す値を変える機能です。
この場合は、xがintなので、aには1が代入されます。
xは確定でintにしかならないじゃんと思ったかもしれません。
この構文が真価を発揮するのは#defineマクロと組み合わせたときです。
#define TYPE_NAME(x) \
_Generic( \
(x), \
int: "int", \
char: "char", \
float: "float", \
double: "double", \
default: "unknown" \
)
int main() {
int i = 0;
const char* type_of_i = TYPE_NAME(i);
char c = 'a';
const char* type_of_c = TYPE_NAME(c);
}
この場合、TYPE_NAME(something)はすべてマクロとして展開され、
_Generic(
(something),
int: "int",
char: "char",
float: "float",
double: "double",
default: "unknown"
)
と書いたのと同じになります。
つまり、type_of_iには"int"、type_of_cには"char"が入ります!
型によって分岐する関数(風マクロ)が作れるということです。
番外編
int main() {
int i = 0;
i = i++;
}
こういうコードで何が起こるか考えてみます。
まず、iを0にします。
次に、iを1追加した値で上書きして、上書きする前の値をiに入れます(←?)
iはどうなるでしょう?
...
...
...
正解は、「何が起こるかわからない」でした!
このような場合、C言語では未定義動作(UB: Undefined Behavior)となります。
C言語の仕様書には、「未定義動作」が定義されています。
3.5.3
1 undefined behavior
behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements
2 Note 1 to entry: Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message)
訳してみるとこんな感じでしょうか
3.5.3
1 未定義動作
移植性のない、または誤ったプログラム構成要素や誤ったデータを使用した結果、この文書においていかなる要件も課せられない動作のこと。
2 項目注記1: 未定義の動作の可能性のある範囲は、状況を完全に無視して予測不能な結果となる場合から、診断メッセージの有無にかかわらず、環境特有の文書化された方法で翻訳またはプログラム実行中に動作する場合、あるいは診断メッセージとともに翻訳または実行を終了する場合まで多岐にわたります。
要は、未定義の動作が発生した後のプログラムでは何が起こってもいいし、警告メッセージを出しても出さなくてもいいし、コンパイルに失敗してもいいし、実行を終了してもいいし、何をしたっていいということが書いてあります。
よくジョークで「未定義のコードを実行した結果コンパイラは何をしてもいい。鼻から悪魔が飛び出しても仕様に反しない」とか言われてたりします。恐ろしいですね。
おわり
おわりです。
Discussion