none
CStringのFormat関数について RRS feed

  • 質問

  • 環境: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のキャストを行わないと落ちないようになります。

    2019年7月10日 12:33

回答

  • (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 );
    }


    こうなっていて、

    1. StringTraits::GetFormattedLengthで生成される文字列長を調べる → 今回の例では11文字
    2. 負ならエラー
    3. GetBufferで生成される文字列が収まるよう領域確保
    4. StringTraits::Formatで文字列を生成 → 今回の例では "あいうえお かきくけこ" となることを期待
    5. ReleaseBufferSetLengthで文字列長を確定

    という手順です。このうちの3.のGetBufferで11文字が格納可能なバッファーに切り替わります。同時に古い文字列 "あいうえお" が捨てられます。ですので、4.のStringTraits::Formatで "あいうえお" を参照しようとしても成功しません。
    # 成功する場合もあるかもしれませんが、それは偶然でしかないため、絶対に避けるべきです。

    参考までに(LPCTSTR)にキャストした場合、次のoperator PCXSTR()関数が呼ばれます。

    operator PCXSTR() const throw()
    {
    	return( m_pszData );
    }

    一目瞭然ですが、何もしておらず、これによりCString::Format()が成功する根拠にはなり得ません。

    2019年7月11日 1:58
  • ごまのあざらしさん、こんにちは。フォーラムオペレーターの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年7月17日 7:16
    モデレータ

すべての返信

  • str1.Format(_T("%s %s"), (LPCTSTR)str1, str2); ですが

    1. (LPCTSTR)str1 の時点でstr1の保持している文字列をスタックに積みます。
    2. str1.Format()はstr1の保持している文字列を開放し、新たな文字列を格納できるよう準備します。
    3. str1.Format()は新たに用意された文字列に対してsprintf()を実行します。
    4. sprintf()はスタックに積まれているstr1の文字列を読もうとします。

    という構造になっているため、2.でstr1の文字列が開放された瞬間に1.でスタックに積んだstr1の文字列が不正な状態になります。

    というわけで、CString::Formatを呼ぶ際は、同変数を引数に与えてはいけません。

    2019年7月10日 12:42
  • 回答ありがとうございます。

    ちなみにですが、

    あるサイトでは、

    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:47
  • (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 );
    }


    こうなっていて、

    1. StringTraits::GetFormattedLengthで生成される文字列長を調べる → 今回の例では11文字
    2. 負ならエラー
    3. GetBufferで生成される文字列が収まるよう領域確保
    4. StringTraits::Formatで文字列を生成 → 今回の例では "あいうえお かきくけこ" となることを期待
    5. ReleaseBufferSetLengthで文字列長を確定

    という手順です。このうちの3.のGetBufferで11文字が格納可能なバッファーに切り替わります。同時に古い文字列 "あいうえお" が捨てられます。ですので、4.のStringTraits::Formatで "あいうえお" を参照しようとしても成功しません。
    # 成功する場合もあるかもしれませんが、それは偶然でしかないため、絶対に避けるべきです。

    参考までに(LPCTSTR)にキャストした場合、次のoperator PCXSTR()関数が呼ばれます。

    operator PCXSTR() const throw()
    {
    	return( m_pszData );
    }

    一目瞭然ですが、何もしておらず、これによりCString::Format()が成功する根拠にはなり得ません。

    2019年7月11日 1:58
  • 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:51
  • AppendFormat使えば済む話かと。 #今時Vc98使ってるならないですが。

    jzkey

    2019年7月12日 3:55
  • ごまのあざらしさん、こんにちは。フォーラムオペレーターの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年7月17日 7:16
    モデレータ