思うだけで学ばない日記 2.0

思うだけで学ばない日記から移転しました☆!よろしくお願いします。

ostringstreamめっちゃ便利よ(1)

C言語プログラムでの文字列の扱いは悩ましいところだが、ある関数でちょっと動的にメッセージを生成して呼び出し元に戻したい、というとき、呼び出し元の方で明示的にバッファを用意するのが一般的だ
そうでない次のような書き方は違法だ

#include <stdio.h>

static char* HelloWorldObsolete()
{
    char str[128];
    sprintf_s(str, sizeof(str), "%s", "Hello World!");
    return str;
}

void TestObsolete()
{
    printf("%s\n", HelloWorldObsolete());
}

HelloWorldObsolete()のスタックフレーム内にとられる自動変数strのスコープはHelloWorldObsolete()内であり、HelloWorldObsolete()を抜けると内容は保証されない
実際、上のコードでは、TestObsolete()内でまずHelloWorldObsolete()が評価され、抜けた直後、printf()呼び出しのためスタックにいろいろ積まれるからスタック上のstr
(の残骸)は一瞬で上書きされ、printf()の内部には文字列"Hello World!"はまず確実に渡らない

この考えからすると、次のプログラム(ただしC++で書かれている)が合法だというのは一瞬理解に苦しむかもしれない

#include <sstream>
#include <iostream>

#if 0
using namespace std;            // 下4行を一括で済ませたいならこっち
#else
using std::ostringstream;
using std::string;
using std::endl;
using std::cout;
#endif

static string HelloWorld()
{
    ostringstream stream;
    stream << "Hello World!";
    return stream.str();
}

static void Test()
{
    cout << HelloWorld() << endl;
}

ぱっと見、streamのスコープはHelloWorld()内だから、stream.str()が返すstringオブジェクトの寿命もそれに準じてHelloWorld()を抜けた直後にゴミと化すか、あるいはヒープ等に実体がとられており、どこかで明示的にdeleteせねばメモリリークを引き起こす、と思うかもしれない
だが何もしなくていい。C言語の範囲内で書かれた次のコードとほぼ同様に、全く合法である。

#include <stdio.h>

typedef struct {
    char str[128];      // メモリ確保の素朴な実装
} TinyString;

static TinyString HelloWorldBetter()
{
    TinyString str;

    sprintf_s(str.str, sizeof(str.str), "%s", "Hello World!");
    return str;
}

void TestBetter()
{
    printf("%s %s %s\n", HelloWorldBetter().str, "How Are Yor?", "I'm Fine.");
}

単なる配列でなく、TinyString構造体でラップしたstr[]は、HelloWorldBetter()を抜ける際にTinyString構造体まるごとスタックにコピーされてTestBetter()に返される。ここまではCの言語仕様上正当*1

そして何か魔法のような力で第2引数としてprintf()に渡る、、

>何か魔法のような力で

どこが合法じゃ!(ノ`Д´)ノ彡┻━┻゛:∴ (ガシャーン

いや実際GMA0BNは(C++でなくて)Cにおいて上記のようなシチュでスタック上にとられた一時オブジェクトの寿命が言語仕様上どこまでかよく知らないが、現実問題としてCにおいては原則呼び出し元がスタック上に引数を準備するから、HelloWorldBetter()呼び出し直前に呼び出し元がprintf()の引数を準備するべく、HelloWorldBetter()呼び出しの前にHelloWorldBetter()の戻り値専用の領域をスタック上に*2作るしかないだろうから多分安全。printf()呼び出しから戻ってくるまで、HelloWorldBetter()の戻り値を上書きする他者は存在しない。

ちなみに上のコードをC++コンパイルするとして、TinyStringクラスにデストラクタを追加すると、デストラクタがprintf()の呼び出し後に呼ばれるのを確認できる@VC++

ただ、ますます脇道にそれるが、C++において、処理系によってはコンストラクタを直に呼んで作った一時オブジェクトの削除がえらく早いことがある。例えば(TinyStringクラスに適切なコンストラクタを用意したとしても)次の書き方は危険かもしれない。

void TestBetter()
{
    printf("%s %s %s\n", TinyString().str, "How Are Yor?", "I'm Fine.");
}

最適化のバグか言語仕様のバグかはしらないが、printf()呼び出しの前に一時オブジェクト(上のコードではprintf()の第2引数に位置する)のデストラクタが呼ばれるのを(VC++以外の処理系で)見たことがある

まあともかくostringstream::str()の戻り値をそのまま関数の戻り値にすることは合法ってことでおk、
いや知らんけど

*1:K&Rのような化石は除外したとして。

*2:より具体的には、スタックのprintf()の引数用領域よりはボトム側(メモリアドレスで言うと上位側)に。