koturnの日記

転職したい社会人1年生の技術系日記

C言語でラムダを実現する

ここしばらく忙しかったが、時間が出来たので、ここはひとつ、記事を書いてみる。
以前、Twitterに投稿したネタを、解説つきで書いてみようと思う。
テーマは、『C言語のラムダ』

ラムダは最近のプログラミング言語では、ほとんど取り入れられている機能であり、ラムダ無くして快適なプログラミングは出来ないといってもいい(?)
しかし、C言語にはラムダがない、ということで嘆いている人も多いことだろう。
今回は、C言語で使えないはずのラムダをプリプロセッサで実現しようという話である。


C言語は関数ポインタがあり、関数をモノとして扱うことができる。
しかしながら、gccコンパイラ拡張機能プリプロセッサを用いて、少し工夫すれば、C言語でラムダを実現することは可能である。

まず、以下のように定義してみよう。

#define LAMBDA(rettype, ARG_LIST, BODY)          \
({                                               \
   rettype __lambda_funcion__ ARG_LIST { BODY; } \
   __lambda_funcion__;                           \
})

以上のようなマクロを定義することで、以下のような記述が可能となる。

int (*sum)(int, int, int) = LAMBDA(int, (int a, int b, int c), {
  return a + b + c;
});

このように、関数ポインタとしてラムダを実現しているのである。
LAMBDAマクロの第一引数が返り値の型を、第二引数が引数を(丸括弧で囲う必要がある)、第三引数に関数の本体を書く。


タネ明かしをすると、
gccには、複文の式化という拡張機能と、関数内関数定義という拡張機能が存在する。
また、C言語においては、関数ポインタを用いることで、関数をモノとして扱うことができる。
これらのことを用いれば、ラムダを実現することが出来るのである。

まず、複文の式化という機能を解説しよう
中括弧{...}を丸括弧()で囲うことで、中括弧を式、つまり値として扱うことができるようになる。
ここで、式化された複文の値は、複文中の最後の式の文中の式の値となる。
以下の例を見てもらった方が分かりやすいだろう。

int a = ({printf("Hello World"); 2;})  // a = 2となる

これが、複文の式化という機能である。
(これはg++やclangでも可能)


次に、関数内関数定義について解説する。
関数内関数定義はgccでのみ用いることができる。(g++とclangでは不可)

void function(void) {
  int inner_function(int a) {
    return a;
  }
  printf("%d\n", inner_function(2));
}

このように、関数を入れ子にして宣言することがgccでは可能なのである。
このとき、外側の関数より外のスコープからは、内部の関数を参照することはできない。


さて、C言語においては、関数名は、アドレスと等価なのであった。
そのため、関数内で複文を作り、その中で関数内関数定義をして、複文の最後に関数名を置くことで、複文の値が内部の関数でのアドレスとなり、プリプロセッサでもラムダが定義できる。
スコープの関係上、関数名が外部に漏れることは無いので、名前を汚染することは無い。


ところで、関数内関数定義を用いているため、g++ではコンパイル出来ないのであった。
g++においては、ラムダはC++0xの言語機能として実現可能で、しかも関数ポインタに変換可能なのであった。(私はC++の言語仕様に詳しくないので、このあたりのことは各自調べていただきたい...m(__)m)
また、ラムダマクロが使用できないコンパイラをはじくようにプリプロセスを記述すると以下のようになる。

#if defined(__GNUC__)
#  if defined(__cplusplus)  // C++は、C++0xのラムダを用いる
#    define LAMBDA(rettype, ARG_LIST, BODY)           \
       ([&]ARG_LIST -> rettype { BODY; } )
#  elif !defined(__clang__)
#    define LAMBDA(rettype, ARG_LIST, BODY)           \
     ({                                               \
       rettype __lambda_funcion__ ARG_LIST { BODY; }  \
       __lambda_funcion__;                            \
     })
#  endif
#endif

これで、gccとg++の両方に対応できた。


最後にサンプルコードを載せておこう。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(__GNUC__)
#  if defined(__cplusplus)  // C++は、C++0xのラムダを用いる
#    define LAMBDA(rettype, ARG_LIST, BODY)           \
       ([&]ARG_LIST -> rettype { BODY; } )
#  elif !defined(__clang__)
#    define LAMBDA(rettype, ARG_LIST, BODY)           \
     ({                                               \
       rettype __lambda_funcion__ ARG_LIST { BODY; }  \
       __lambda_funcion__;                            \
     })
#  endif
#endif


int main(void) {
  int i;
  int array[5] = {4, 1, 7, -2, 3};
  int len;

  // 関数ポインタに無名関数を渡す例
  int (*sum)(int, int, int) = LAMBDA(int, (int a, int b, int c), {
    return a + b + c;
  });
  // 関数ポインタに格納した無名関数を呼び出す
  printf("2 + 3 + 5 = %d\n", sum(2, 3, 5));

  // その場で無名関数を呼び出す例1
  len = LAMBDA(int, (const char *str1, const char *str2), {
    return strlen(str1) - strlen(str2);
  })("Hello", "World!");
  printf("difflen = %d\n", len);

  // その場で無名関数を呼び出す例2
  printf("6 - 1 = %d\n", LAMBDA(int, (int a, int b), {
    return a - b;
  })(6, 1));

  // qsort()関数の第4引数に渡す例
  qsort(array, (sizeof(array) / sizeof(array[0])), sizeof(array[0]),
    LAMBDA(int, (const void *a, const void *b), {
      return *(int *)a - *(int *)b;
    })
  );
  for (i = 0; i < (sizeof(array) / sizeof(array[0])); i++) {
    printf("array[%d] = %d\n", i, array[i]);
  }
  return 0;
}

実行結果

2 + 3 + 5 = 10
difflen = -1
6 - 1 = 5
array[0] = -2
array[1] = 1
array[2] = 3
array[3] = 4
array[4] = 7