yohhoyの日記

技術的メモをしていきたい日記

文字列取得バッファとしてのstd::string

C++標準ライブラリの文字列型 std::string*1 を、C言語インタフェースへの文字列取得バッファとして使う方法についてメモ。本記事の内容はStack Overflowで見つけた質問と回答に基づく。

2016-03-27追記:C++17(C++1z)対応版はこちら→文字列取得バッファとしてのstd::string リターンズ - yohhoyの日記

2013-08-30追記:C++03以前でも事実上「std::string 内の文字列バッファは連続したメモリ領域」といえる?C++ Standard Library Defect Report List #530 Must elements of a string be contiguous?、Cringe not: Vectors are guaranteed to be contiguousコメントも参照のこと。

まとめ:

  • C++98/03標準ライブラリ仕様上、実現不可能。連続性が保障されている配列型か std::vector で代用のこと。
  • C++11標準ライブラリ仕様ならば安全に実現可能。ただしNUL終端文字の扱いに注意。
  • 注意:C++11準拠コンパイラでも、標準ライブラリ内部実装はC++03相当というケースに留意。*2

下記例のようなレガシーAPIに対して、配列の代わりに std::string オブジェクトを文字列取得バッファとして渡す方法。

// バッファsにNUL終端文字列を取得(size=バッファ長)
// 戻り値:バッファに格納された文字数(NUL文字を除く)
int legacy_get_string_api(char *s, int size);

// plain C
#define BUFSIZE  /*適当なサイズ*/
char buf[BUFSIZE];
legacy_get_string_api( buf, BUFSIZE );
// 最大(BUFSIZE - 1)文字を取得

C++98/03

C++98/03標準ライブラリでは、std::string の内部実装として「不連続メモリ領域に文字データを保持する(rope)」などの最適化技法が許容されていた。このため string::operator[] 結果のアドレスをとっても、連続した内部バッファ領域を指すという保証がなく、legacy_get_string_api() と組み合わせて利用することはできない。
→ 2013-08-30追記:標準規格による明確な保証がないが、実際の処理系では問題無く動作すると期待できる?NUL終端文字に関する注意点はC++11と同様。

// C++03準拠標準ライブラリ
#include <string>
std::string buf(BUFSIZE, '\0');
legacy_get_string_api( &buf[0], BUFSIZE );  // NG!!
// &buf[0], &buf[1], &buf[2],... は連続配置されている保証がない
// 上記例では不定メモリ領域を上書きして壊す危険性がある

std::vector クラスであれば連続した内部バッファ領域を取得でき、ポインタ値を legacy_get_string_api() に直接渡せる。ただし別途 std::string クラスへの変換処理が必要なため、この string オブジェクトの構築処理と vectorstring で2倍メモリが必要になるオーバーヘッドは不可避。

#include <vector>
#include <string>
std::vector<char> buf(BUFSIZE);
int len = legacy_get_string_api( &buf[0], BUFSIZE );  // OK
std::string str(&buf[0], len);  // vectorからstringに変換

C++11

C++11標準ライブラリでは、std::string 内部バッファは連続領域に配置されることが保証される(21.4.5/p2)。また c_str() および data() メンバ関数により、いつでもNUL終端された変更不可文字列へのポインタ(const char*)を取得できる*3。このため、長さ N の文字列を保持する string 内部バッファでは、実際にはNUL終端文字分を含めた (N + 1) バイト確保されていると推測される。(この場合でも、size()メンバ関数は文字列長 N を返す。)

一方C++11標準規格によれば、文字位置 0≦n<size() ならば stringオブジェクト外部から書き換え可能だが、NUL終端文字が格納されている n=size() 位置は書き換えが禁止される。例:文字列 "ABC" を格納した string オブジェクトに対して、s[0]='a'*(s.begin()+2)='c'などは可能だが、たとえNUL終端文字の上書き(s[3]='\0')であってもs[3]の書き換えは未定義動作(undefined behavior)を引き起こす*4。(21.4.5/p2)
→ 2021-02-03追記:C++17以降はstring内部バッファのNUL終端文字位置へのNUL終端文字上書きに限って特例許容される。詳細はyumetodoさんによる こちらの記事 を参照のこと。

// C++11準拠標準ライブラリ
#include <string>
std::string buf(BUFSIZE, '\0');
int len = legacy_get_string_api( &buf[0], BUFSIZE );  // OK
buf.resize(len);
// BUFSIZE=10 かつ 文字列"123456789"/len=9 が取得できた場合、
// resize前の内部バッファ={"123456789\0",'\0'}と推測される。

N3337 21.4.1/p5, 21.4.5/p1-2より引用(下線部は強調)。

5 The char-like objects in a basic_string object shall be stored contiguously. That is, for any basic_string object s, the identity &*(s.begin() + n) == &*s.begin() + n shall hold for all values of n such that 0 <= n < s.size().

const_reference operator[](size_type pos) const;
reference operator[](size_type pos);
1 Requires: pos <= size().
2 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.

おまけ:Microsoft way

// Microsoft MFC/ATL
#include <atlstr.h>

CString szBuffer;
char *p = szBuffer.GetBuffer(BUFSIZE);
legacy_get_string_api(p, BUFSIZE);
szBuffer.ReleaseBuffer();

関連URL:

*1:正確にはstd::basic_stringクラステンプレート。本記事では簡単のため std::string で説明する。

*2:gcc/libstdc++など。http://gcc.gnu.org/ml/gcc/2011-10/msg00115.htmlも参照のこと。

*3:c_str()およびdata()はconstメンバ関数であり、C++11標準規格ではこれらを呼び出しても “stringオブジェクト内の文字要素を指すポインタ値” が無効化されないことが保証される(N3337 21.4.1/p6)。なおC++03以前では、明示的に「c_str()またはdata()メンバ関数呼び出しでポインタ値が無効されうる」と定義されていた(ISO C++03 21.3/p5)。

*4:NUL終端文字位置はmodify操作のみ禁止される。s[3]=='\0' などのreadアクセスであれば問題ない。