トップ回答者
CStringのFormat関数について

質問
-
環境:visual studio 2005
言語:C++
CStringのFormat関数で例外となる場合があります。
原因おわかりでしょうか。
ご教示お願いいたします。
以下サンプルコード
CString str1 = "あいうえお";
CString str2 = "かきくけこ"
str1.Format(_T("%s %s"), (LPCTSTR)str1, str2); ←ここで落ちる時があります。
で、
CString strTemp;
strTemp.Format(_T("%s %s"), str1, str2);
str1.Empty();
str1.Format(_T("%s"), strTemp);
LPCTSTRのキャストを行わないと落ちないようになります。
回答
-
(LPCTSTR)でキャストしても、開放(emptyではなく、delete?)されてしまうため、アクセス違反となってしまうということでしょうか。
前後の説明等を確認できていませんが、「(LPCTSTR)でキャストをすれば大丈夫」という投稿には何の根拠もないように見受けられます。
もう一度説明すると、str1オブジェクトは1つの文字列しか保持・管理していません。初期状態では"あいうえお"を保持していました。Format完了時点では、新たに生成された文字列("あいうえお かきくけこ"が期待される)を保持します。問題はこの文字列がいつ切り替わるか、です。
実際のCString::Format(の中で呼び出しているCString::FormatV)のソースコードを挙げます。
void FormatV( _In_z_ _Printf_format_string_ PCXSTR pszFormat, _In_ va_list args) { ATLASSERT( AtlIsValidString( pszFormat ) ); if(pszFormat == NULL) AtlThrow(E_INVALIDARG); int nLength = StringTraits::GetFormattedLength( pszFormat, args ); if (nLength < 0) AtlThrow(E_FAIL); PXSTR pszBuffer = this->GetBuffer( nLength ); StringTraits::Format( pszBuffer, nLength+1, pszFormat, args ); this->ReleaseBufferSetLength( nLength ); }
こうなっていて、
- StringTraits::GetFormattedLengthで生成される文字列長を調べる → 今回の例では11文字
- 負ならエラー
- GetBufferで生成される文字列が収まるよう領域確保
- StringTraits::Formatで文字列を生成 → 今回の例では "あいうえお かきくけこ" となることを期待
- ReleaseBufferSetLengthで文字列長を確定
という手順です。このうちの3.のGetBufferで11文字が格納可能なバッファーに切り替わります。同時に古い文字列 "あいうえお" が捨てられます。ですので、4.のStringTraits::Formatで "あいうえお" を参照しようとしても成功しません。
# 成功する場合もあるかもしれませんが、それは偶然でしかないため、絶対に避けるべきです。参考までに(LPCTSTR)にキャストした場合、次のoperator PCXSTR()関数が呼ばれます。
operator PCXSTR() const throw() { return( m_pszData ); }
一目瞭然ですが、何もしておらず、これによりCString::Format()が成功する根拠にはなり得ません。
- 回答としてマーク ごまのあざらし 2019年10月2日 10:05
-
ごまのあざらしさん、こんにちは。フォーラムオペレーターのHarukaです。
MSDNフォーラムにご投稿くださいましてありがとうございます。
CStringTクラステンプレートのドキュメントから:「PCXSTR関数の引数の代わりにCStringTオブジェクトを自由に代用することができます。」
関数パラメーターが定数Cスタイルの文字列を必要とするたびに、CStringTオブジェクトを渡すことができます。
これは、オペレーターPCXSTR()を呼び出すことによって暗黙的に変換されます。
一方、可変長引数を持つ関数は、型指定されていない引数リストを取ります。
このシナリオでは、PCXSTRが(意味的に)期待されている場所にCStringTオブジェクトを渡すのは安全ではありません。
コンパイラは、暗黙の変換を実行するためのコードを生成する必要があることを知る手段がありません。
自分で変換を実行するのは、オペレーターPCXSTR()を呼び出すか、またはGetString()クラスメンバを呼び出すことです。
CSimpleStringTクラステンプレートは、サポートされているすべての文字列クラスの基本です。
それはタイプPCXSTRの単一の(私用)データメンバを含み、仮想関数を含まず、それを標準レイアウトタイプにします。
したがって、バイナリ表現は、その唯一のメンバー、つまりゼロで終了する文字配列へのポインターと同じです。
CSimpleStringT::operator PCXSTR: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/csimplestringt-class?view=vs-2019#operator_pcxstr
CStringT Class: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/cstringt-class?view=vs-2019
CSimpleStringT Class: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/csimplestringt-class?view=vs-2019#operator_pcxstrどうぞよろしくお願いします。
MSDN/ TechNet Community Support Haruka
ご協力くださいますようお願いいたします。また、MSDNサポートに賛辞や苦情がある場合は、MSDNFSF@microsoft.comまでお気軽にお問い合わせください。~- 回答としてマーク ごまのあざらし 2019年10月2日 10:05
すべての返信
-
str1.Format(_T("%s %s"), (LPCTSTR)str1, str2); ですが
- (LPCTSTR)str1 の時点でstr1の保持している文字列をスタックに積みます。
- str1.Format()はstr1の保持している文字列を開放し、新たな文字列を格納できるよう準備します。
- str1.Format()は新たに用意された文字列に対してsprintf()を実行します。
- sprintf()はスタックに積まれているstr1の文字列を読もうとします。
という構造になっているため、2.でstr1の文字列が開放された瞬間に1.でスタックに積んだstr1の文字列が不正な状態になります。
というわけで、CString::Formatを呼ぶ際は、同変数を引数に与えてはいけません。
-
回答ありがとうございます。
ちなみにですが、
あるサイトでは、
str1.Format(_T("%s %s"), str1, str2);
はダメ、
str1.Format(_T("%s %s"), (LPCTSTR)str1, str2);
(LPCTSTR)でキャストをすれば大丈夫
と投稿されていました。
>2.でstr1の文字列が開放された瞬間
(LPCTSTR)でキャストしても、開放(emptyではなく、delete?)されてしまうため、
アクセス違反となってしまうということでしょうか。
よろしくお願いいたします。
- 編集済み ごまのあざらし 2019年7月10日 23:56
-
(LPCTSTR)でキャストしても、開放(emptyではなく、delete?)されてしまうため、アクセス違反となってしまうということでしょうか。
前後の説明等を確認できていませんが、「(LPCTSTR)でキャストをすれば大丈夫」という投稿には何の根拠もないように見受けられます。
もう一度説明すると、str1オブジェクトは1つの文字列しか保持・管理していません。初期状態では"あいうえお"を保持していました。Format完了時点では、新たに生成された文字列("あいうえお かきくけこ"が期待される)を保持します。問題はこの文字列がいつ切り替わるか、です。
実際のCString::Format(の中で呼び出しているCString::FormatV)のソースコードを挙げます。
void FormatV( _In_z_ _Printf_format_string_ PCXSTR pszFormat, _In_ va_list args) { ATLASSERT( AtlIsValidString( pszFormat ) ); if(pszFormat == NULL) AtlThrow(E_INVALIDARG); int nLength = StringTraits::GetFormattedLength( pszFormat, args ); if (nLength < 0) AtlThrow(E_FAIL); PXSTR pszBuffer = this->GetBuffer( nLength ); StringTraits::Format( pszBuffer, nLength+1, pszFormat, args ); this->ReleaseBufferSetLength( nLength ); }
こうなっていて、
- StringTraits::GetFormattedLengthで生成される文字列長を調べる → 今回の例では11文字
- 負ならエラー
- GetBufferで生成される文字列が収まるよう領域確保
- StringTraits::Formatで文字列を生成 → 今回の例では "あいうえお かきくけこ" となることを期待
- ReleaseBufferSetLengthで文字列長を確定
という手順です。このうちの3.のGetBufferで11文字が格納可能なバッファーに切り替わります。同時に古い文字列 "あいうえお" が捨てられます。ですので、4.のStringTraits::Formatで "あいうえお" を参照しようとしても成功しません。
# 成功する場合もあるかもしれませんが、それは偶然でしかないため、絶対に避けるべきです。参考までに(LPCTSTR)にキャストした場合、次のoperator PCXSTR()関数が呼ばれます。
operator PCXSTR() const throw() { return( m_pszData ); }
一目瞭然ですが、何もしておらず、これによりCString::Format()が成功する根拠にはなり得ません。
- 回答としてマーク ごまのあざらし 2019年10月2日 10:05
-
MSDNのCStringT::Format()の説明には
引用-->
The call will fail if the string object itself is offered as a parameter to Format. For example, the following code will cause unpredictable results:
CAtlString str = _T("Some Data");
str.Format(_T("%s%d"), str, 123);
// Attention: str is also used in the parameter list.
<--引用
とあります。つまり
「文字列オブジェクト自体がFormatのパラメータとして提供されている場合、呼び出しは失敗します。たとえば、次のコードは予測不可能な結果を引き起こします。(Google翻訳)」
という説明があるわけです。
次に、operator LPCTSTR()は自身の所有する文字列バッファである this->m_pszData のポインタを戻すだけで、何も特別なことはしません。
以上からthis->Format( 「フォーマット文字列」, [args…])のargsにはthisだけでなく、this->m_pszDataも含まれると読み取るべきだと思います。
- 編集済み 仲澤@失業者 2019年7月11日 2:52
-
ごまのあざらしさん、こんにちは。フォーラムオペレーターのHarukaです。
MSDNフォーラムにご投稿くださいましてありがとうございます。
CStringTクラステンプレートのドキュメントから:「PCXSTR関数の引数の代わりにCStringTオブジェクトを自由に代用することができます。」
関数パラメーターが定数Cスタイルの文字列を必要とするたびに、CStringTオブジェクトを渡すことができます。
これは、オペレーターPCXSTR()を呼び出すことによって暗黙的に変換されます。
一方、可変長引数を持つ関数は、型指定されていない引数リストを取ります。
このシナリオでは、PCXSTRが(意味的に)期待されている場所にCStringTオブジェクトを渡すのは安全ではありません。
コンパイラは、暗黙の変換を実行するためのコードを生成する必要があることを知る手段がありません。
自分で変換を実行するのは、オペレーターPCXSTR()を呼び出すか、またはGetString()クラスメンバを呼び出すことです。
CSimpleStringTクラステンプレートは、サポートされているすべての文字列クラスの基本です。
それはタイプPCXSTRの単一の(私用)データメンバを含み、仮想関数を含まず、それを標準レイアウトタイプにします。
したがって、バイナリ表現は、その唯一のメンバー、つまりゼロで終了する文字配列へのポインターと同じです。
CSimpleStringT::operator PCXSTR: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/csimplestringt-class?view=vs-2019#operator_pcxstr
CStringT Class: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/cstringt-class?view=vs-2019
CSimpleStringT Class: https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/reference/csimplestringt-class?view=vs-2019#operator_pcxstrどうぞよろしくお願いします。
MSDN/ TechNet Community Support Haruka
ご協力くださいますようお願いいたします。また、MSDNサポートに賛辞や苦情がある場合は、MSDNFSF@microsoft.comまでお気軽にお問い合わせください。~- 回答としてマーク ごまのあざらし 2019年10月2日 10:05