yohhoyの日記

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

std::stringとリスト初期化の小さな罠

C++標準ライブラリの文字列型std::stringと、リスト初期化(list initialization)の組み合わせによる落とし穴。

2個の文字列リテラルによるstd::stringのリスト初期化は、ほとんどのC++処理系においてコンパイル時には問題検知されないが*1、実行時に未定義動作(undefined behavior)を引き起こす。

#include <string>

std::string s1 = { "abc" };  // OK: s1 == "abc"
std::string s2 = { "abc", "xyz" };  // NG: 未定義動作

std::stringの厳密な型はstd::basic_string<char, std::char_traits<char>, std::allocator<charT>>となる。簡単のためstd::allocator<charT>>を単にAと略記する。

  • s1 ではコンストラクbasic_string(const char*, const A& = A())が選択される。第1引数に渡されるポインタ値は有効なヌル終端文字列を指すためwell-defined。
  • s2 では文字列リテラルからポインタ型const char*へ暗黙変換され(2.14.5/p8, 4.2/p1)、テンプレートコンストラクtemplate<class II> basic_string(II begin, II end, const A& = A())が選択される(II=const char*)。第1, 2引数に渡されるポインタ値の比較演算は未規定(unspecified)であり(5.9/p2)*2、半開区間 [begin, end) は有効な区間となる保証がない。これは標準ライブラリ要件に違反するため、プログラムは未定義動作を引き起こす。

C++11 2.14.5/p12, 21.4.2/p14-15, 23.2.3/p3より一部引用。

Whether all string literals are distinct (that is, are stored in nonoverlapping objects) is implementation-defined. (snip)

template<class InputIterator>
  basic_string(InputIterator begin, InputIterator end,
               const Allocator& a = Allocator());

Effects: If InputIterator is an integral type, equivalent to basic_string(static_cast<size_type>(begin), static_cast<value_type>(end), a)
Otherwise constructs a string from the values in the range [begin, end), as indicated in the Sequence Requirements table (see 23.2.3).

In Tables 100 and 101, X denotes a sequence container class, (snip), i and j denote iterators satisfying input iterator requirements and refer to elements implicitly convertible to value_type, [i, j) denotes a valid range, (snip)

関連URL

*1:C++標準の範囲内ではコンパイル時に問題を検知するのは困難に思える。最適化器による定数伝播(constant propagation)の結果として、“異なる文字列リテラルの要素を指すポインタ値同士の比較” を検知できれば可能性はあるかも?

*2:偏執的な解釈では、std::string s = {"abc", "abc"}; であれば処理系定義(implementation-defined)で有効かつ空文字列に初期化される。ここではC++処理系の最適化処理により文字列リテラル "abc" が同一配列として表現されることを期待している。