C言語の険しい記法,仕様

これらの記法,仕様はクソと言われることも多いが,扱いによっては非常に有用な場合もある. また,C言語で書かれたライブラリ等を読んでいると出てくることもあるので,少なくとも読めて損は無いだろう. 脳内のパーサを育てるのに活用してほしいし,意味わからん仕様を書き足してほしい.

でも公式の仕様とか読んだほうが正確なので参考程度で

演算子

単項演算子

まずはこれを見て欲しい.

int a = -10;

おそらくこの一行に違和感を感じる事は無いだろう. しかし,ここで-は単項演算子として扱われている.

単項演算子としての-はオペランドの負数を生成する.

一方,単項演算子としての+も存在している.

int a = +10;

ANSIで追加された仕様で,実質的にオペランドをそのまま生成している. また,別の用法として,

intより小さい型をintへ変換するのにも使える.

三項演算子

個人的にこれは扱いによって非常に有用な場合もある記法の顕著な例だと考えている.

簡単に説明すると,

if (hoge) {
    fuga();
} else {
    piyo();
}

のようなコードを

hoge ? fuga() : piyo();

のように一行で書ける.

ネストすると読みづらいと思うから気を付けろ.

エルビス演算子

三項演算子とほぼ同じ.

hoge() ? hoge() : piyo();

のように,左値が真であるときはそのまま返したい時,二重に評価を行うのは冗長である. そんな時,

hoge() ?: piyo();

とすることで同じ結果を得られるうえ,左値の評価も一度で済む.

カンマ演算子

C言語クソ仕様ではよく挙げられる. C以外でも,C++はもちろん,JSでもある仕様. ,は演算子であり,左から右に評価される.また,演算の戻り値は最後の式の値となる.

例文

#define swap(a, b) ((a) ^= (b), (b) ^= (a), (a) ^= (b))

これはカンマ演算子の左の式は評価された上で捨てられる事を応用し,

a ^= b;
b ^= a;
a ^= b;

のように,複数行に渡る処理を一行にまとめている. 他にも,後の式の値が返される事を利用して,

return perror(errno), NULL;

のような書き方もできる(可読性が損なわれる可能性が高いので,注意が必要)

for文の中で使われがち

for (int i = 0, j = 0; j < length; i++, j++) {
    fuga();
}

構文

関数定義のK&R記法

C言語の教科書として悪名高い有名なK&R本で書かれていたらしい記法. 関数の仮引数の型をあとから書ける.

int hoge(a, b)
    int a;
    int b;
{
    fugapiyo;
}

一見気持ち悪いが,古いコードではよく見られる記法の為,読めるようにしておいて損は無い. 状況によっては可読性が向上する場合もある

トライグラフ

今ではまず目にしないと思うが,極稀に文字列リテラルで意図しない表示になったりした時はこの仕様の可能性がある.

ISO646に共通して含まれる文字だけでコードを書くための表記法

簡単に書くと,

トライグラフの表記 表される文字
??= #
??( [
??/ \
??) ]
??' ^
??< {
??! |
??> }
??- ~

処理がエスケープシーケンスやトークンの解釈よりも先に行われる為,

printf("(キレてないが??)\n");

とすると,

(キレてないが]

と表示されるコンパイラもある.(手元のgccでは,-trigraphsを有効にしないとトライグラフは適用されなかった)

メンバ,インデックス指定代入

決してクソ仕様では無いと思っているが,入門書には書かれていない事がある.

int array[5] = {1, 0, 2, 0, 3};

int array[5] = {[0] = 1, [2] = 2, [4] = 3};

のようにインデックスを指定して初期化できる.

また,構造体に於いても

struct hoge {
    int x;
    int y;
} a = { // 構造体を定義し,構造体変数をそのまま定義
    .x = 10,
    .y = 20,
};

と書ける

複合リテラル

無名オブジェクトを表現するためのリテラル

(型名){初期化子}の形で表現される

sample

#include <stdio.h>

void iter_print(int *array, int len) {
    for (int i = 0; i < len; i++)
        i<len-1?printf("%d, ", array[i]):printf("%d\n", array[i]);
}

typedef struct {
    int x;
    int y;
} hoge;

void print_hoge(hoge h) {
    printf("hoge {\n\tx: %d\n\ty: %d\n}\n", h.x, h.y);
}

int main() {
    iter_print((int []){1, 2, 3, 4, 5}, 5); // ここが複合リテラル
    /*
        > 1, 2, 3, 4, 5
    */
    print_hoge((hoge){10, 20});
    /*
        > hoge {
                  x: 10
                  y: 20
          }
    */
}

未定義動作系

未定義動作はコンパイルエラーも発生しないから面倒だよね

副作用完了点について

tldr

式の評価によって副作用が発生する事がある.

副作用完了点←かっこいい

特定のタイミングでそれ以前のすべての評価による副作用が完了しており,かつ,その後の評価の副作用が発生していない点を副作用完了点と呼ぶ.

副作用が発生するような式の評価では,副作用完了点が処理の途中に存在しない限り,それらの評価順序に依存してはいけない

定義された副作用完了点

非常に簡単な例として,

i = i+1;
a[i] = i;

のような文は許されるが,次のような文は許されない

i = ++i + 1; // iが副作用完了点の前で2回変更されている
a[i++] = i; // 左辺値式で,格納される値を決定するため以外の目的でiを読み取っている

また,関数呼び出しの引数間のコンマは演算子では無いため,副作用完了点ではない(これは未定義の動作ではなく,未規定の動作である(関数の引数の評価順序は規定されていない)). よって,以下のような文は許されない

f(i++, i);