リンク
0回目(初回):こちら
1回目(今回):こ↑こ↓
2回目(次回):まだ無い
はじめに
**これなに???**という人は0回目のを読んでね!
相変わらずくだらない理由で規格を漁っているL3Sotaです。先日ECMAScript2016の規格もダウンロードしたのでJavascriptもやる可能性が出てきました。
さて、今回は大したことない問題で盛大にグダリます。
お題はというと、こんな感じのコードが発端です:
# include <iostream>
# include <string>
int main(void) {
std::string s = "Hello";
std::cout << s.substr(0) << '\n'; // "Hello"
std::cout << s.substr(4) << '\n'; // "o"
std::cout << s.substr(5); // ""
std::cout << std::endl;
return 0;
}
まあこれ我が家のgccでもClangでも普通にコンパイルしました。あれ、でも最後のやつって s.substr(s.size()); だよね?大丈夫??
はい、**気になったら規格を読もう!**とりあえずsubstrを見ましょう!
と、その前に:
string は basic_string!
まず std::string::substr ってそのままだと無いんですよね。 string っていうクラス直書きではまず存在しないんですけど。探してみれば std::basic_string::substr ならあるんですけど…。(むーりぃー
じゃあ string の定義って何だっけということになりますが、安心してください、規格にちゃんと書いてあります。
N3242 (p. 623), N3797 (p. 632):
21.3 String classes [string.classes]
The header<string>defines thebasic_stringclass template [. . .] and four typedefs,string,u16string,u32string, andwstring, that name the specializationsbasic_string<char>,basic_string<char16_t>,basic_string<char32_t>, andbasic_string<wchar_t>, respectively.
N3242 (p. 627), N3797 (p. 634):
typedef basic_string<char> string;
typedef basic_string<char16_t> u16string;
typedef basic_string<char32_t> u32string;
typedef basic_string<wchar_t> wstring;
つまり basic_string をテンプレートとして、普段使っている string はテンプレート引数に普通の char を放り込んだ「特殊化」(specialization)というやつです。なので残りの話は全部 basic_string で行えばいいということです。やったね!
ほんなら早速、遠慮なく:
basic_string::substr
N3242 (p. 649), N3797 (p. 657):
21.4.7.8
basic_string::substr[string::substr]
basic_string<charT,traits,Allocator>
substr(size_type pos = 0, size_type n = npos) const;
Requires:
pos <= size()
Throws:out_of_rangeifpos > size().
Effects: Determines the effective lengthrlenof the string to copy as the smaller ofnandsize() - pos.
Returns:basic_string<charT,traits,Allocator>(data()+pos,rlen).
一応 pos == size() はセーフらしいです。また、この場合は rlen == 0 になりますね。 basic_string<charT,traits,Allocator>(data()+pos,rlen) を読み解く必要がありますが、まず basic_string という名前なので constructor です。引数の data() は string の中身の配列でしょうが、記事の完成度の為にもちゃんと規格を見ましょう:
basic_string::data
N3242 (p. 645), N3797 (p. 654)
21.4.7.1
basic_stringaccessors [string.accessors]
const charT* c_str() const noexcept;
const charT* data() const noexcept;
Returns: A pointer
psuch thatp + i == &operator[](i)for eachiin[0,size()].
Complexity: constant time.
Requires: The program shall not alter any of the values stored in the character array.
配列、もといポインターが返ってくるようです。そして!どうやら i == size() でもまともな値が返ってくることが保証されています。詳しい挙動は
operator[] に盥廻しされてるので次はそっちを見ます:
basic_string::operator[]
N3242 (p. 637):
21.4.5
basic_stringelement access [string.access]
const_reference operator[](size_type pos) const noexcept;
reference operator[](size_type pos) noexcept;
Requires:
pos <= size().
Returns:*(begin() + pos)ifpos < size(). Otherwise, returns a reference to an object of typecharTwith valuecharT(), where modifying the object leads to undefined behavior.
Complexity: constant time.
N3797 (p. 646) 差分:
const_reference operator[](size_type pos) const;
reference operator[](size_type pos);
Returns: [. . .]
Throws: Nothing.
Complexity: [. . .]
規格の間に若干差が出ていますね。N3797では noexcept が削られ、_Throws:_の部分が追記されています。Exception投げないんで存在意義微妙ですけど。何はともあれ、どうやら operator[] は pos==size() ならば
charT() の値を返すらしいです。 string の場合は即ち char のことなので、え~なんともややこしいですね。Primitive type ですし。この辺は初期化の話になってくるので次回の記事に回しましょう!(え
結論だけ言わないと次に進めないのでそこまで飛ばしますが、結果としては値はゼロになる、とのことです。つまり、 substr の返り値の表現にあった data()+pos は '\0' へのポインターであることがわかる、ということです。
さて、このポインターと rlen==0 を basic_string<charT,traits,Allocator>(data()+pos,rlen) に放り込んだらどうなるのでしょう?規格を見ましょう:
basic_string::basic_string
N3242 (pp. 633-634):
21.4.2
basic_stringconstructors and assignment operators [string.cons]
basic_string(const charT* s, size_type n,
const Allocator& a = Allocator());
Requires:
sshall not be a null pointer andn < npos.
Effects: Constructs an object of classbasic_stringand determines its initial string value from the array ofcharTof lengthnwhose first element is designated bys, as indicated in Table 66.
Table 66 — basic_string(const charT*, size_type, const Allocator&) effects
| Element | Value |
|---|---|
data() |
points at the first element of an allocated copy of the array whose first element is pointed at by s
|
size() |
n |
capacity() |
a value at least as large as size()
|
N3797 (pp. 641-642) 差分:
Requires:
spoints to an array of at leastnelements ofcharT.
'\0' がコピーされるかどうかは微妙なところですが、長さゼロの string はちゃんと作られるっぽいです。コピーされるか微妙と言っているのは何も不思議な話ではなく、"array of charT of length n"という表現がそもそも意味不明であることに起因します。全く同じわけではありませんが、規格では配列の宣言に関して以下の制約が明記されています:
配列の長さは正(それはそう)
N3242 (p. 188), N3797 (p. 187):
8.3.4 Arrays [dcl.array]
In a declaration
T DwhereDhas the form
D1 [constant-expression_opt]attribute-specifier-seq_opt
[. . .]. If the constant-expression (5.19) is present, it shall be an integral constant expression and its value shall be greater than zero.
N3797 (p. 187) 差分:
If the constant-expression (5.19) is present, it shall be a converted constant expression of type
std::size_tand its value shall be greater than zero.
はい、長さゼロの配列は宣言できまーーーせん!まあコードの中では当然の話ですよね。規格の英語でたまたま長さゼロの配列という表現になってしまったからといって超困るわけではありませんが、少なくとも '\0' がコピーされているかどうかはわかりませんね。
とりあえずこれで冒頭の s.substr(5) に問題は無いことがわかりました。めでたしめでたし。ただこれはこれでまだ調べ甲斐のありそうな点があります。何かと言えばそう、長さゼロの string をそのまま出力しても大丈夫なことです。
謎が謎を呼ぶ(跋文)
発端となったコードが完全に正しいか確認するためには、あとは長さゼロの string を出力できるかどうかにかかっているのですが、記事名が substr なのに内容ぶっ飛びすぎじゃね?という懸念(及び今日は書き疲れたという大変深刻な問題)により別記事に分けようと思います。初期化の話も積み残しているのでやる事はまだまだありそうです。とりあえずここまでお付き合いいただきありがとうございます。
自分から規格読むとか言っておきながらアレですが、割と規格をMarkdownに整形するのがダルいので眠くて解説を間違えていたりするかもしれません。その場合はコメントかTwitterにでも文句を書いてあげてください。可及的気が向いたら直します。
では、次の記事が満足いく形になるまで。