yohhoy氏のブログを要約する
レガシーなC APIとやり取りするときに文字列を書き込んでもらうための領域を確保して、その領域の先頭のポインタと大きさを渡す、なんてことがままある。
// バッファsにNUL終端文字列を取得(size=バッファ長)
// 戻り値:バッファに格納された文字数(NUL文字を除く)、バッファが十分な大きさではなかった場合NULL文字を含む必要サイズ
int legacy_get_string_api(char *s, int size);
そんなとき大抵はstd::basic_string
にして取り扱いたいはずだ。ここで実はstd::basic_string
自体がその文字列バッファとして使えるということがよく知られている。日本語圏なら @yohhoy さんのブログが詳しい。
さて、こうしたAPIでは文字列バッファを渡すところにnullptr
を渡すと必要なメモリーの大きさを戻り値で返してくれることが多い。
#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提案文書P0272R0より該当箇所を引用(下線部は強調)。
charT* data() noexcept;
Returns: A pointerp
such thatp + i == &operator[i]
for eachi
in[0,size()]
.
Complexity: Constant time.
Requires: The program shall not alter the value stored atp + size()
.
NULL文字を書き込むことすら許容されていないとわかる。
LWG Issue 2475
しかしよく考えてほしい。この制約はあくまでNULL文字が破壊されると困るからという制約であって、NULL文字を書き込むことは認められてもいいはずだ。
というわけでそういうIssueが出た。statusをみるとC++17になっているので、C++17規格書に適用されると読める。残念ながらC++17を参照するときに見るn4659の文面は間に合っていない。
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 thancharT()
; otherwise, the behavior is undefined.
charT()
、つまりはNULL文字を除いては、というように緩和されたことがわかる。
つまり冒頭のコードは
#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で動かそうと格闘している。そんなときたまたまこんなコードが目に入った。
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);
-
length - 1
ってなんだ、NULL文字のとこ書き換わってね?それってだめじゃ・・・? - 修正commitを作りかけてcommit messageでyohhoy氏のブログを引っ張る
- でもやっぱり規格書の文面で殴るべきだよなぁ
- C++20の規格を読む
- other than
charT()
ってまじ???OpenSiv3Dの実装あってるやんけ!yohhoy氏のブログ古い情報やんけ! - これは記事書くしかないやろ