C言語で関数のオーバーロード

これはAmusement Creators Advent Calendar (ACAC) の6日目の記事です.

adventar.org

C++C#にあってC言語にはない機能の一つにオーバーロード (overloading) というのがあります. これはC言語でもCPP(CPreProcessor)を使えば限定的ではあるけれどもオーバーロードミミックできる!という内容の記事です.

オーバーロードって何?

オーバーロードってなんなんでしょう?とりあえずC++でどう定義されているのか見てみます.
現行C++ (C++17) のdraftは多分 n4659*1 なのでこれを読んでみると16章で大々的にoverloadingについて言及されているのが見つかります.
冒頭の部分を訳すと"同じスコープにある一つの名前に対して2つ以上の異なる宣言が指定されているとき,その名前はオーバーロードされているという. 付け加えて,同じスコープで同じ名前に対する型の違う宣言が2つあったときそれらはオーバーロード宣言と呼ばれる.関数と関数テンプレートの宣言だけがオーバーロードできて変数や型の宣言はオーバーロードできない" でしょうか.
多分このあたりは他の言語でもだいたい同じなんじゃないかなーと思います.例を出すと

#include <iostream>
void hoge(int) { std::cout << "neko" << std::endl; }
void hoge(char) { std::cout << "inu" << std::endl; } // valid
void hoge(int, int) { std::cout << "syounen" << std::endl; } // valid
// void hoge (int) { std::cout << "piyopiyo" << std::endl; }  ! invalid ! 型が同じ宣言はダメ

int main() {
    hoge(42); // neko
    hoge('a'); // inu
    hoge(42, 'c'); // syounen
}

ってかんじです.
別のことをする関数に同じ名前を付けるのは良くないとかオーバーロードがあるような言語は型推論が決定不能になるといった理由から単純なオーバーロードという機能はあまり好まれていないフシがありますが,まぁこれはオアソビですしC言語には型推論なんて無いのであんまり気にせずいきましょう.

本題

CPPを使ってC言語でもオーバーロードみたいなのを書けるようにしたいんですが,CPPはトークン列しか見れないので型によるオーバーロードはできません.なので引数の違いによるオーバーロードで勘弁して下さい.残念.

実際のコードはこんなかんじです

void hoge1(int n) { printf("neko %d\n", n); }
void hoge2(int n, char c) { printf("inu %d%c\n", n, c); }

#define SELECTER(ONE_, TWO_, SELECT, ...) SELECT
#define HOGE(...) SELECTER(__VA_ARGS__, hoge2, hoge1) (__VA_ARGS__)

int main() {
    HOGE(42);  // neko 42
    HOGE(42, 'a'); // inu 42a
}

マクロHOGEに渡した引数の数が1つならhoge1が,2つならhoge2が呼ばれていることがわかります.
どうしてこのような挙動をするのでしょう?
引数が1つのときを考えるとHOGE(42)SELECTER(42, hoge2, hoge1) (42)に展開されて次にSELECTER(42, hoge2, hoge1)の部分がhoge1に展開されるので最終的にhoge1(42)になります.
引数が2つのときを考えるとHOGE(42, 'a')SELECTER(42, 'a', hoge2, hoge1) (42, 'a')に展開されて次にSELECTER(42, 'a', hoge2, hoge1)の部分がhoge2に展開されるので最終的にhoge2(42, 'a')になります.
つまりSELECTERは前から3番目を取り出すマクロで,そこにHOGE__VA_ARGS__を渡すことでHOGEの引数の数に応じてSELECTERが選ぶトークンがずれるようになってるんですね.
同じテクニックを使えば与えられた引数の数を計算するマクロも作れそうですね.つまりこうです

#include <stdio.h>

#define SELECTER(ONE, TWO, THREE, FOUR, N, ...) N
#define N_ARGS(...) SELECTER(__VA_ARGS__, 4, 3, 2, 1)

int main() {
    printf("%d\n", N_ARGS(neko));  // 1
    printf("%d\n", N_ARGS(neko, inu, syounen));  // 3
}

この例では4つまでしか引数の数を計算できませんがもっと数を増やせば増やした分だけ計算できる引数の数を大きくすることができます.

参考にしたもの