背景と質問

以下のようなプログラムを見掛けました。

char *token1 = strtok(str, " ");
char *token2 = strtok(NULL, " ");
char *token3 = strtok(NULL, " ");

if(token1 == NULL) {
    // トークンがない場合の処理
}
else if(token2 == NULL) {
    // トークンが1個の場合の処理
    ...
}
else if(token3 == NULL) {
    // トークンが2個の場合の処理
    ...
}
else {
    // トークンが3個以上の場合の処理
    ...
}

このプログラムでは、strtok() が一度 NULL を返した後、続けて strtok(NULL, ...) を呼ぶことができる、ということが前提になっています。
こういった使い方は今まで思い付かず、考えたことがない、というより無意識のうちに未定義と思い込んでいたのですが、このような使い方は規格上保証されているものなのでしょうか。

規格調査

検証のために、規格を調べてみました。
ISO/IEC 9899:2011 の 7.24.5.8 The strtok function から引用します (強調は引用者による)。
この引用中の s1s2 は、それぞれ strtok の第1引数、第2引数を表します。

3
The first call in the sequence searches the string pointed to by s1 for the first character
that is not contained in the current separator string pointed to by s2. If no such character
is found, then there are no tokens in the string pointed to by s1 and the strtok function
returns a null pointer
. If such a character is found, it is the start of the first token.

4
The strtok function then searches from there for a character that is contained in the
current separator string. If no such character is found, the current token extends to the
end of the string pointed to by s1, and subsequent searches for a token will return a null
pointer
. If such a character is found, it is overwritten by a null character, which
terminates the current token. The strtok function saves a pointer to the following
character
, from which the next search for a token will start.

5
Each subsequent call, with a null pointer as the value of the first argument, starts
searching from the saved pointer and behaves as described above.

この記述について、以下のように場合分けして考えてみました。

ここで、パラグラフ5の書き方は曖昧に感じましたが、strtok(NULL, ...) 呼び出しについては、パラグラフ3と4の s1 を パラグラフ4で保存したポインタに置き換えて実行する、という意味であると解釈しました。

ケース1: s1 = "abc def" のように、トークン文字で終わる場合

"def" が返った後の呼び出しで、パラグラフ 4 の「subsequent searches for a token will return a null pointer」のパターンに合致し、NULL が返ります。「search」が複数形となっているため、何度 strtok(NULL, ...) を呼んでも NULL が返ります。

ケース2: s1 = "abc def " のように、トークン列の末尾に区切り文字がある場合

"def" が返った後の呼び出しで、パラグラフ 3 の「strtok function returns a null pointer」のパターンに合致し、NULL が返ります。何かしら保存したポインタ (このケースでは文字列末尾) があり、そのポインタのアップデートがないため、何度呼んでもパラグラフ3のパターンに合致して NULL が返ります。

ケース3: s1 = " " のように、トークン列がない場合

前項と同様、パラグラフ3のパターンに合致して NULL が返ります。この場合、保存したポインタというものがないため、後続の strtok(NULL, ...) は未定義になりそうです。

以上、場合によっては NULL が返った後の strtok(NULL, ...)NULL が返り、別の場合では未定義になりそうに思えました。
しかし、それでは何とも中途半端で気持ち悪く感じます。
これをどう考えるか、ということで、以下の2通りの可能性を考えました。

  • 上記の解釈に間違いがあり、NULL が返るか未定義かはどちらか一方に定まる。
  • 基本的に未定義と考えるべき。パラグラフ4で 「search」が複数形になっていることに特に意味はない。

しかし、何が正しいのかという確証は取れませんでした。

実験

参考までに、手持ちの環境ではどうなのか実験してみました。

環境:

  • OS: Gentoo Linux
  • コンパイラ: gcc 4.8.4, clang 3.5.0
  • ライブラリ: glibc 2.20

プログラム:

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

// トークンの内容を出力。ただし NULL の場合は "(null)" を出力。
#define PR_TOKEN(token)  do { printf(#token " = %s\n", (token)? token: "(null)"); } while(0)

int main(void)
{
    // ケース1: 文字列がトークン文字で終わる
    char str1[] = "abc def";
    char *token1_1 = strtok(str1, " ");
    char *token1_2 = strtok(NULL, " ");
    char *token1_3 = strtok(NULL, " ");
    char *token1_4 = strtok(NULL, " ");
    PR_TOKEN(token1_1);
    PR_TOKEN(token1_2);
    PR_TOKEN(token1_3);
    PR_TOKEN(token1_4);

    // ケース2: 文字列が区切り文字で終わる
    char str2[] = "abc def ";
    char *token2_1 = strtok(str2, " ");
    char *token2_2 = strtok(NULL, " ");
    char *token2_3 = strtok(NULL, " ");
    char *token2_4 = strtok(NULL, " ");
    PR_TOKEN(token2_1);
    PR_TOKEN(token2_2);
    PR_TOKEN(token2_3);
    PR_TOKEN(token2_4);

    // ケース3: トークン列がない
    char str3[] = " ";
    char *token3_1 = strtok(str3, " ");
    char *token3_2 = strtok(NULL, " ");
    char *token3_3 = strtok(NULL, " ");
    char *token3_4 = strtok(NULL, " ");
    PR_TOKEN(token3_1);
    PR_TOKEN(token3_2);
    PR_TOKEN(token3_3);
    PR_TOKEN(token3_4);

    return 0;
}

結果:

token1_1 = abc
token1_2 = def
token1_3 = (null)
token1_4 = (null)
token2_1 = abc
token2_2 = def
token2_3 = (null)
token2_4 = (null)
token3_1 = (null)
token3_2 = (null)
token3_3 = (null)
token3_4 = (null)

すべてのケースについて、一度 NULL が返った後の strtok() 呼び出しで NULL が返っています。
今回、正当性について疑問が残るケース3について、ケース1 、ケース2で保存していた情報が残っているため、たまたまうまく動いている、ということを疑い、ケース3のみを実行したり、ケース2の最初の strtok() 呼び出しの直後にケース3に入ってみても同様でした。