Linux
macro
kernel
C言語

すごいC言語のマクロ __is_constexpr

はじめに

linuxカーネルで使われているC言語マクロの紹介記事を見て、自分もソースコードを眺めていたら面白いマクロを見つけたので紹介します。

マクロの機能

include/linux/kernel.h
#define __is_constexpr(x) \
        (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

非常に短いマクロですがこのマクロはxが定数式であるかどうかを判定します。
定数式とは、例えば配列の宣言のときに使えるやつらです
実際試してみると、

test.c
#include<stdio.h>
#define Def 10
#define __is_constexpr(x) \
            (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

enum test{Enum};
int main(){
    int Val = 10;
    const int Const_val = 10;

    int a = __is_constexpr(Val);
    int b = __is_constexpr(Const_val);

    int c = __is_constexpr(10);
    int d = __is_constexpr(Def);
    int e = __is_constexpr(Enum);
    printf("%d %d %d %d %d\n",a,b,c,d,e);
    return 0;
}
output
0 0 1 1 1

変数を入れると0を返しますが、ソースに書き込まれている数や、defineされている数、enumの場合は1を返す、つまりtrueになっていることが分かります。

原理

マクロの本体は次のようになっています

macro
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

まず、次の部分に着目します

(long)(x) * 0l

これは、xが定数式であれば定数式の0であり、そうでなければ定数式ではない0です。

((void *)((long)(x) * 0l)) 

次に先程の値を(void *)にキャストします。ここでC言語の規格を確認すると

Nullポインタ定数とは、定数式である0またはそれを(void *)にキャストしたもの。

と書いてあります。定数式の0を(void *)にキャストするとNullポインタ定数になります。しかし、定数式でない0をキャストしてもNullポインタ定数にはなりません。0番地を差すポインタとなります1

8 ? ((void *)((long)(x) * 0l)) : (int *)8

するとこのコードはxが定数式のとき

8 ? [ヌルポインタ定数] : (int *)8

となり、xが定数式でないと

8 ? [voidを指すポインタ] : (int *)8

となります。

この三項演算子の型を考えます。普通、二番目の式と三番目の式は同じ型でなければなりません。一方がNULLであったりしたらどうなるのか、c言語のマニュアルには次のように書いてあります。

一方がNullポインタ定数で、もう一方がポインタであったら、もう一方の型を返す。

一方がvoidを指すポインタで、もう一方がポインタであったら、voidでない方のポインタがvoidへのポインタに変換されて、voidへのポインタを返す。

つまりxが定数式であったら三項演算子はintへのポインタを返し、そうでないとvoidへのポインタを返します。

以上より、xが定数式のときマクロの本体は

(sizeof(int) == sizeof(*[intへのポインタ])

定数式でないときは

(sizeof(int) == sizeof(*[voidへのポインタ])

となります。sizeof(void)は1であるので、前者はtrueで後者はfalseです。定数式であるかどうかが判別できました。

更にいうと、sizeof演算子の比較の結果は定数式になると定められており、このマクロは定数式を返すことになります。

test.c
#include<stdio.h>
#define __is_constexpr(x) \
                (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

int main(){
    if( __is_constexpr(__is_constexpr(10))){
        printf("yes\n");
    }else{
        printf("no\n)");
    }
    return 0;
}
output
yes

まとめ

c言語の規格の隅をついたすごいマクロです。LinuxカーネルのようなC言語で書かれた巨大なプログラムは、少ない言語機能で非常に多彩なことを実現しており驚くばかりです。


  1. Nullポインタというのはどこも指していないことが保証されているポインタであり、大抵の実装では0番地ですが規格上はどこでも良いものです