LoginSignup
4
2

More than 5 years have passed since last update.

C,C++規格冒険記1:std::string::substr

Last updated at Posted at 2017-04-26

リンク

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を見ましょう!

と、その前に:

stringbasic_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 the basic_string class template [. . .] and four typedefs, string, u16string, u32string, and wstring, that name the specializations basic_string<char>, basic_string<char16_t>, basic_string<char32_t>, and basic_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 if pos > size().
Effects: Determines the effective length rlen of the string to copy as the smaller of n and size() - 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 that p + i == &operator[](i) for each i 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) if pos < size(). Otherwise, returns a reference to an object of type charT with value charT(), 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==0basic_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 and n < npos.
Effects: Constructs an object of class basic_string and determines its initial string value from the array of charT of length n whose first element is designated by s, 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 least n elements of charT.

'\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 where D 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にでも文句を書いてあげてください。可及的気が向いたら直します。
では、次の記事が満足いく形になるまで。

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2