C言語で安全に数値を複数含む文字列を分解しつつ整数型に変換して読み込む方法

# はじめに

https://qiita.com/yumetodo/items/238751b879c09b56234b

以前C言語で安全に標準入力から数値を取得するのは極めて困難であるという記事を執筆しました。

つまるところ`strtol`系関数で文字列から数値への変換を行う必要があるわけですが、だいぶ面倒なので特に競技プログラミングなんかの文脈では`scanf`を普通に使ってしまうことと思います。

さて、競技プログラミングではよく次のような入力が与えられます。なお`H`と`W`の最大値、`e`の最大最小値は与えられるものとします。

```
H W
e11 e12 ... e1W
e21 e22 ... e2W
...
eH1 eH2 ... eHW
```

ここでこれらの入力を`scanf`で捌くのはあんまり効率的ではないのではないかという疑問が出てきます。

scanf関数は書式文字列を渡してパース方法を指定します。　$H \times W$ 回scnafするということは、その分の書式文字列パース作業が加算されるということです。

https://qiita.com/asana_yui/items/2e603a154b6924cce31d

この記事では`strtok`で文字列を分割してから`atoi`で変換するということをやっていましたが、`atoi`は`scanf`と同じ問題を抱えている(安全ではない)うえに、`strtok`は入力文字列を書き換えるため扱いにくいわけです。

# `strtol`系関数はパースに使えます

```c
long int strtol(
const char * restrict nptr,
char ** restrict endptr,
int base);
```

`strtol`関数の第2引数が今回の主役です。

ここには`char*`型変数へのポインタを渡します。そして`strtol`関数は処理が終わると第1引数に与えた文字列のどこまでパースしたかを伝えるべく、パース終了地点へのポインタを第2引数経由で渡されている`char*`型変数に書き込みます。例を見てみましょう(`errno`はエラーハンドリングに必要ですがその処理はここでは省略します)。

```c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>

int main(void)
{
    const char* str = "111 222";
    char* endptr;
    //errno = 0;
    const long re = strtol(str, &endptr, 10);
    assert(endptr == str + 3);
    printf(
        "re = %d\n"
        "endptr = %p\n"
        "str + 3 = %p\n"
        "endptr(rest string): \"%s\"",
        (int)re, endptr, str + 3, endptr
    );
    return 0;
}
```

https://wandbox.org/permlink/fZF2G2OoofaczhQv

```plaintext:実行例
re = 111
endptr = 0x40068b
str + 3 = 0x40068b
endptr(rest string): " 222"
```

`str`を見たとき、`111`の部分が整数型へ変換できる部分です。直後の` `が最初に現れる整数型へ変換できない部分です。この位置をポインタを使って表すと`str + 3`の位置となります(`&str[3]`でも同じ意味です)。

`strtol`関数実行後の`endptr`はまさしく`str + 3`の位置を指し示しています。


この性質を利用して文字列の分解と整数型への変換を一挙に行うことができます。

# 実装

というわけで取るべき戦略は、

1. 標準入力からは`fgets`を用いて行ごとに読み込む
1. 読み込んだ文字列をいい感じにパースしていく

というわけでそのパースする関数を作りました。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <stdbool.h>
#include <errno.h>
#include <ctype.h>
/**
 * @brief 区切り文字列によって区切られている数値文字列をパースする
 * @param dest 結果を受け取る配列へのポインタ
 * @param dest_size `dest`の配列の大きさであり、`input`から数値を抜き出す回数
 * @param input 入力文字列
 * @param endptr_out パース終了時の残りの文字列の先頭へのポインタを格納する`const char*`型変数領域へのポインタ
 * @param delims 区切り文字列
 */
bool parse_line_l(long* dest, size_t dest_size, const char* input, const char** const endptr_out, int radix, const char* delims)
{
    if (dest == NULL || input == NULL || delims == NULL) return false;
    const char* endptr = input;
    const char* s = input;
    for (size_t i = 0; i < dest_size; ++i, s = endptr) {
        errno = 0;
        const long re = strtol(s, (char**)&endptr, radix);
        if (errno != 0 || (re == 0 && s == endptr)) return false;
        while (strchr(delims, endptr[0])) ++endptr;
        if (endptr_out != NULL) *endptr_out = endptr;
        dest[i] = re;
    }
    return true;
}
int main(void)
{
    const char* input = "111 222, 333, 444";
    const char* endptr;
    long arr[3];
    if (!parse_line_l(arr, sizeof(arr)/sizeof(*arr), input, &endptr, 10, ", ")) return 1;
    printf("%ld, %ld, %ld, rest: \"%s\"", arr[0], arr[1], arr[2], endptr);
    return 0;
}
```

https://wandbox.org/permlink/GVVubXSN3rVkDOfM

まずstrtol関数によってパースを試みます。失敗している条件は`errno`が`0`ではないもしくは戻り値が`0`で
かつ`endptr`が入力文字列と同じ時です。

次に区切り文字列を読み飛ばします。`strchr`関数は文字列の中に任意の文字を含むか調べることができますが、これを用いて入力文字列を1文字づつ読み飛ばしていきます。