strtok() が NULL を返した後、さらに strtok(NULL, ...) を呼んでもよいのか。
背景と質問
以下のようなプログラムを見掛けました。
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 から引用します (強調は引用者による)。
この引用中の s1
、s2
は、それぞれ strtok
の第1引数、第2引数を表します。
3
The first call in the sequence searches the string pointed to bys1
for the first character
that is not contained in the current separator string pointed to bys2
. If no such character
is found, then there are no tokens in the string pointed to bys1
and thestrtok
function
returns a null pointer. If such a character is found, it is the start of the first token.4
Thestrtok
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 bys1
, 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. Thestrtok
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に入ってみても同様でした。