リンク
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_string
class 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_range
ifpos > size()
.
Effects: Determines the effective lengthrlen
of the string to copy as the smaller ofn
andsize() - 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_string
accessors [string.accessors]
const charT* c_str() const noexcept;
const charT* data() const noexcept;
Returns: A pointer
p
such thatp + i == &operator[](i)
for eachi
in[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_string
element 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 typecharT
with 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_string
constructors and assignment operators [string.cons]
basic_string(const charT* s, size_type n,
const Allocator& a = Allocator());
Requires:
s
shall not be a null pointer andn < npos
.
Effects: Constructs an object of classbasic_string
and determines its initial string value from the array ofcharT
of lengthn
whose 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:
s
points to an array of at leastn
elements 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 D
whereD
has 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_t
and its value shall be greater than zero.
はい、長さゼロの配列は宣言できまーーーせん!まあコードの中では当然の話ですよね。規格の英語でたまたま長さゼロの配列という表現になってしまったからといって超困るわけではありませんが、少なくとも '\0'
がコピーされているかどうかはわかりませんね。
とりあえずこれで冒頭の s.substr(5)
に問題は無いことがわかりました。めでたしめでたし。ただこれはこれでまだ調べ甲斐のありそうな点があります。何かと言えばそう、長さゼロの string
をそのまま出力しても大丈夫なことです。
謎が謎を呼ぶ(跋文)
発端となったコードが完全に正しいか確認するためには、あとは長さゼロの string
を出力できるかどうかにかかっているのですが、記事名が substr
なのに内容ぶっ飛びすぎじゃね?という懸念(及び今日は書き疲れたという大変深刻な問題)により別記事に分けようと思います。初期化の話も積み残しているのでやる事はまだまだありそうです。とりあえずここまでお付き合いいただきありがとうございます。
自分から規格読むとか言っておきながらアレですが、割と規格をMarkdownに整形するのがダルいので眠くて解説を間違えていたりするかもしれません。その場合はコメントかTwitterにでも文句を書いてあげてください。可及的気が向いたら直します。
では、次の記事が満足いく形になるまで。