24
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

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

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 pointer p such that p + i == &operator[i] for each i in [0,size()].
Complexity: Constant time.
Requires: The program shall not alter the value stored at p + size().

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

LWG Issue 2475

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

というわけでそういうIssueが出た。statusをみるとC++17になっているので、C++17規格書に適用されると読める。残念ながらC++17を参照するときに見るn4659の文面は間に合っていない

Issue 2475: Allow overwriting of std::basic_string terminator with charT() to allow cleaner interoperation with legacy APIs - WG21 LWG Issues

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文字を除いては、というように緩和されたことがわかる。

つまり冒頭のコードは

#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);
  1. length - 1ってなんだ、NULL文字のとこ書き換わってね?それってだめじゃ・・・?
  2. 修正commitを作りかけてcommit messageでyohhoy氏のブログを引っ張る
  3. でもやっぱり規格書の文面で殴るべきだよなぁ
  4. C++20の規格を読む
  5. other than charT()ってまじ???OpenSiv3Dの実装あってるやんけ!yohhoy氏のブログ古い情報やんけ!
  6. これは記事書くしかないやろ
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
24
Help us understand the problem. What are the problem?