C++17時代における文字列取得バッファとしてのstd::string

# yohhoy氏のブログを要約する
レガシーなC APIとやり取りするときに文字列を書き込んでもらうための領域を確保して、その領域の先頭のポインタと大きさを渡す、なんてことがままある。

```cpp
// バッファsにNUL終端文字列を取得（size＝バッファ長）
// 戻り値：バッファに格納された文字数(NUL文字を除く)、バッファが十分な大きさではなかった場合NULL文字を含む必要サイズ
int legacy_get_string_api(char *s, int size);
```

そんなとき大抵は`std::basic_string`にして取り扱いたいはずだ。ここで実は`std::basic_string`自体がその文字列バッファとして使えるということがよく知られている。日本語圏なら @yohhoy さんのブログが詳しい。

- [文字列取得バッファとしてのstd::string - yohhoyの日記](https://yohhoy.hatenadiary.jp/entry/20130413/p1)
- [文字列取得バッファとしてのstd::string リターンズ - yohhoyの日記](https://yohhoy.hatenadiary.jp/entry/20160327/p1)

さて、こうしたAPIでは文字列バッファを渡すところに`nullptr`を渡すと必要なメモリーの大きさを戻り値で返してくれることが多い。

```cpp
#include <string>
// NULL文字を含む必要なメモリーの大きさが帰る
const auto len = legacy_get_string_api(nullptr, 0);
std::string buf(len, '\0');
legacy_get_string_api(buf.data(), len);  // OK
buf.resize(len - 1);
```

上のコードを見てほしい。変数`len`はNULL文字を含む大きさである。そして最終行では`std::basic_string::resize()`を呼び出している。一見すごく冗長に見える。最初からNULL文字の分を省いておけばいいのでは？？？

実はそうではない。yohhoy氏ブログから引用する。

> ただしNUL終端文字が配置される文字位置*2に関しては、C++14以前と同様に書き換えが禁止されることに注意。
>
> > 一方C++11標準規格によれば、文字位置 0≦n＜size() ならば string オブジェクト外部から書き換え可能だが、NUL終端文字が格納されている n＝size() 位置は書き換えが禁止される。
> > [文字列取得バッファとしてのstd::string](http://d.hatena.ne.jp/yohhoy/20130413/p1)
>
> 提案文書P0272R0より該当箇所を引用（下線部は強調）。
>
> > `charT* data() noexcept;`
    *Returns*: A pointer `p` such that `p + i == &operator[i]` for each `i` in `[0,size()]`.
    *Complexity*: Constant time.
    *Requires*: <u>The program shall not alter the value stored at `p + size()`</u>.

NULL文字を書き込むことすら許容されていないとわかる。

# LWG Issue 2475

しかしよく考えてほしい。この制約はあくまでNULL文字が破壊されると困るからという制約であって、NULL文字を書き込むことは認められてもいいはずだ。

というわけでそういうIssueが出た。statusをみるとC++17になっているので、C++17規格書に適用されると読める。残念ながらC++17を参照するときに見る[n4659の文面は間に合っていない](https://timsong-cpp.github.io/cppwp/n4659/string.accessors#6)。

[Issue 2475: Allow overwriting of std::basic_string terminator with charT() to allow cleaner interoperation with legacy APIs - WG21 LWG Issues](https://wg21.cmeerw.net/lwg/issue2475)

# C++17時代における文字列取得バッファとしてのstd::string

では当該部分はどうなったのか。n4659の文面は間に合っていないのでC++20を参照するときに見るn4861を見てみる。

> https://timsong-cpp.github.io/cppwp/n4861/string.accessors#6
>
> *Remarks*: The program shall not modify the value stored at `p + size()` to any value **other than `charT()`; otherwise, the behavior is undefined.**

`charT()`、つまりはNULL文字を除いては、というように緩和されたことがわかる。

つまり冒頭のコードは

```cpp
#include <string>
// NULL文字を含む必要なメモリーの大きさが帰る
const auto len = legacy_get_string_api(nullptr, 0);
std::string buf(len - 1, '\0');
legacy_get_string_api(buf.data(), len);  // OK
```

のようになる。

# まとめ

もう文字列取得バッファとしてstd::basic_stringを使うときにresizeしなくていい！

# 記事執筆の背景

最近OpenSiv3Dをmingwで動かそうと格闘している。そんなときたまたまこんなコードが目に入った。

https://github.com/Siv3D/OpenSiv3D/blob/ea191438ecbc64185f5df3d9f79dffc6757e4192/Siv3D/src/Siv3D-Platform/WindowsDesktop/FileSystem/SivFileSystem_Platform.cpp#L566-L574

```cpp
				std::wstring result2(length - 1, L'\0');
				const DWORD length2 = ::GetCurrentDirectoryW(length, result2.data());

				if ((length2 == 0) || (length2 > length))
				{
					return FilePath();
				}

				return detail::NormalizePath(Unicode::FromWString(result2), true);
```

1. `length - 1`ってなんだ、NULL文字のとこ書き換わってね？それってだめじゃ・・・？
2. 修正commitを作りかけてcommit messageでyohhoy氏のブログを引っ張る
3. でもやっぱり規格書の文面で殴るべきだよなぁ
4. C++20の規格を読む
5. other than `charT()`ってまじ？？？OpenSiv3Dの実装あってるやんけ！yohhoy氏のブログ古い情報やんけ！
6. これは記事書くしかないやろ