Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

C++でstd::stringをどう返すべきか Part1

スポンサードリンク

f:id:fa11enprince:20200701030835j:plain
C++では関数からstd::stringを皆はどうやって返してんだろうっていう疑問がわきました。
フツーに考えてstd::stringをそのまま値で返す一択なのですが、
そこで問題になるのがコピーコスト……
っていうかそもそもコピーしてるの?とかいろいろ疑問が沸くわけです。
動作確認環境はg++ 4.8.3です。

実験用のコード

普通はこんなのなしだけどstringを継承したデバッグ用のクラス

#include <iostream>

class mystring : public std::string {
public:
    // コンストラクタ
    mystring() : std::string()
    {
        std::cout << "mystring constructor"
            << " mystring() called!" << std::endl;
    }
    mystring(const char *str) : std::string(str)
    {
        std::cout << "mystring constructor"
            << " mystring(const char *str) called!" << std::endl;
    }
    mystring(const std::string &s) : std::string(s)
    {
        std::cout << "mystring constructor"
            << " mystring(const std::string &s) called!" << std::endl;
    }

    // コピーコンストラクタ
    mystring(const mystring &rhs) : std::string(rhs.c_str())
    {
        std::cout << "mystring copy constructor"
            << " mystring(const mystring &rhs) called!" << std::endl;
    }

    // 代入演算子
    mystring &operator=(const char *str)
    {
        std::cout << "mystring assignment operator"
            << " mystring &operator=(const char *str) called!" << std::endl;
        std::string::operator=(str);
        return *this;
    }
    mystring &operator=(const std::string& s)
    {
        std::cout << "mystring assignment operator"
            << " mystring &operator=(const std::string& s) called!" << std::endl;
        std::string::operator=(s);
        return *this;
    }
    mystring &operator=(const mystring& rhs)
    {
        std::cout << "mystring assignment operator"
            << " mystring &operator=(const mystring& rhs) called!" << std::endl;
        std::string::operator=(rhs.c_str());
        return *this;
    }
};

このクラスでいろいろやってみます。

パターン1(test1)

mystring &test1()
{
    mystring str = "test";
    std::cout << "function test1 called!!" << std::endl;
    return str;
}

これはダメです!呼び元で破棄されたオブジェクトを参照しています。
初学者がやりがちなミスです。
ローカルで確保した変数を参照で返すというものです。
今だいたいメジャーな言語(CやC++除く)では基本的にヒープ領域に確保して、そうやって取った領域はスコープを抜けても生きていて、その生存期間はGCが判断していつの間にやら回収してくれるまるで魔法のような仕組みになっている……のでやりがちです。

パターン2(test2)

mystring *test2()
{
    std::cout << "function test2 called!!" << std::endl;
    mystring *str = new mystring("test");
    return str;
}

問題ないコードだけど使用者にメモリ解放の責任を負わせるのでよくないです。

パターン3(test3)

mystring test3()
{
    std::cout << "function test3 called!!" << std::endl;
    mystring str = "test";
    return str;    
}

int main()
{
    mystring str3 = test3();
}

まぁ普通ですよね。これがスタンダードなやり方だと思います。
ただ、コピーコストがあるのか?とか気になります。

実行結果

function test3 called!!
mystring constructor mystring(const char *str) called!

RVOが効いて無駄なコピーが発生していません。

パターン4(test4)

mystring test4()
{
    std::cout << "function test4 called!!" << std::endl;
    return mystring("test");
}

int main()
{
    mystring str4 = test4();
}

実行結果

function test4 called!!
mystring constructor mystring(const char *str) called!

test3と特に差がないです。

パターン5(test5)

mystring &test5(mystring &s)
{
    s = "test";
    return s;
}

int main()
{
    mystring str5;
    str5 = test5(str5);
}

実行結果

mystring constructor mystring() called!
mystring assignment operator mystring &operator=(const char *str) called!
mystring assignment operator mystring &operator=(const mystring& rhs) called!

参照渡ししているパターンです。もはやreturnと関係がなくなってきた…。

パターン6(test6)

参照をもらって書き換えるパターンです。

void test6(mystring &s)
{
    s = "test";
}

int main()
{
    mystring str6;
    test6(str6); 
}

実行結果

mystring constructor mystring() called!
mystring assignment operator mystring &operator=(const char *str) called!

なんかstringとかの中身を変えたいならこれでいい気が指摘ました。

一連のものはここに置いてあります。
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

RVOについて

1.呼び出し側では f を格納する場所を確保 (確保するだけ、コンストラクタは呼ばない)
2.そのアドレスを関数 foo に渡す
3.関数のほうでは、戻値用のオブジェクトのアドレスが渡ってきた場合 (かつ、それが NULL でない場合) は、 新たに一時変数を生成したりはせずに、 そのアドレスから始まる領域に対してコンストラクタを起動する

http://www.fides.dti.ne.jp/~oka-t/cpplab-retval-ctor.html

まとめ

test3とtest4の例はreturnしたときにコピーコンストラクタが動かず、
RVO(Retrun Value Optimization)が効いています。

id:yohhoyさんにありがたい情報をいただいてPart2を書きます。
Part2でどうすればいいか結論が出ました。

非常に見にくかったので全面的に修正しました。

最近の状況等を踏まえてまとめました