memorandums

日々のメモです。

c++のコピー動作について

数日前から背骨に激痛が。。。嫁さんの勧めで昨日はスポーツ整体屋さんに行ってきました。保険対象外のため5千円くらいしましたが痛みが消えました。しかし。。。今朝起きてみるとまた痛みが復活。連休はおとなしくしています。

さてさて。

ある方がc++で作られたプログラムを読む機会がありました。前職ではcかc++でしたが、大学に来てからはJava。cを書く場面がなく記憶の彼方へ。

その方の作ったプログラムでは静的なclass変数のコピーをバンバン使われていました。しかもコンストラクタやデストラクタなしで。

前職でc++を使うときにはコンストラクタやコピーコンストラクタを作って、引数や戻り値では参照渡しにしたりconstつけたり、メモリリークしないように気を使った覚えがあります。会社だったため高価なPurify使って検証したりもしていました。10年以上前の話なので古い仕様しか頭にありません。最近のc++11とかc++14といった流れは追いかけていませんし製作経験もありません。

で、思い出すためにとりあえず簡単な実験をしてみました。

結論はこちらのページに書かれていました。僕が感じた違和感は動的メモリ確保をするプログラムの場合に適用される話で、静的な宣言のみであればc++のコピーのおかげでプリミティブ型でもクラス型?でもstructと同様に値がコピーされることがわかりました(というか思い出しました)

実験です。

クラスAにint配列のvalueをおき、setメソッドで任意の数値をvalueに書き込みます。で、mainで静的に宣言した配列変数同士でコピーしたときに、クラスA内のvalueもコピーされるか?という検証です(静的でも動的でも結局はインスタンス同士のコピーがどのようにされるのか?ということですね)

#include <iostream>

class A {
public:
    int value[3];
    void set(int start) {
        for (int i = 0; i < 3; i++) {
            value[i] = start + i;
        }
    }
};

int main(int argc, const char * argv[]) {
    A a[2];
    
    a[0].set(1);
    a[1].set(3);
    a[0] = a[1];
    return 0;
}

xcodeで上記を実行すると以下のようになりました。クラスのインスタンス間ではデフォルトでコピーが実行されるようです。戻り値としても気持ち悪い。。。ですが。

(lldb) print a
(A [2]) $0 = {
[0] = {
value = ([0] = 3, [1] = 4, [2] = 5)
}
[1] = {
value = ([0] = 3, [1] = 4, [2] = 5)
}
}

でも、プリミティブ型の配列間ではコピーできません。

int main(int argc, const char * argv[]) {
    int a[3] = {1,2,3};
    int b[3] = {0,0,0};
    a = b;  //コンパイルエラー
    return 0;
}

このコピー機構?が動作することでまずいのがnewでの動的メモリ確保したフィールド変数がクラス内に存在する場合。動的メモリを扱うフィールド変数はポインタ変数になりますので、その内容がコピーされると同じメモリを参照してしまう。。。なのでコピーコンストラクタを使ってメモリを確保しなおし内容を移し替える必要がある、ということでした。

Javaではどうでしょう?「Javaにはポインタはない」という言葉も死語に近いですが。。。学び始めた当初、c++との違いでとても理解に苦しみました。確かにポインタ変数はないけどクラスのインスタンス変数はポインタというか参照型ですしね。。。

class A {
    int value[] = new int [3];
    void set(int start) {
        for (int i = 0; i < 3; i++) {
            value[i] = start + i;
        }
    }
    void show() {
        for (int i = 0; i < 3; i++) {
            System.out.println(value[i]);
        }
    }
};

void setup() {
    A a[] = new A [2];
    a[0] = new A();
    a[1] = new A();
    
    a[0].set(1);
    a[1].set(3);
    a[0] = a[1]; //浅いコピー
    
    a[0].show();
    a[1].show();

    a[1].set(100);

    a[0].show();
    a[1].show();
}

で、結果は以下。ちょっと見にくいですが。。。a[1].set(100)の影響がa[0]とa[1]両方にありました。浅いコピーってやつですね。

3 ←a[0]
4
5
3 ←a[1]
4
5
100 ←a[0]
101
102
100 ←a[1]
101
102

Javaは余計でしたが、もともとのプログラムの断片が以下です。とくに気持ち悪かったのが以下の※のところです。自動変数だからスコープを抜けると消滅するはずで。。。と思ったらその方がバイブルにしている教科書にはこのような書き方がされているのだとか。確かに値は問題ないようです。戻り値を渡すときにtemp変数の内容がスタック上にコピーされるのかと。

const int P = 2;

class A{
    int a;
public:
    A operator+(A x) {
        A temp;
        temp.a = (a + x.a) % P;
        return temp;  //※
    }
    void set(int t){ a = t; }
};

で、ちょっと書き直したのが以下。参考にさせていただいたのはこちらのページです。自動変数はやめてコンストラクトした値を戻しています。気持ちいいです。。。あと引数と戻り値に参照演算子をつけて不必要な値渡しによるコピーを抑えています。

class A{
    int a;
public:
    A(int x = 0) : a(x){}
    A& operator=(const A& x) {
        a = x.a;
        return *this;
    }
    A& operator=(const int x) {
        a = x;
        return *this;
    }
    A operator+(const A& x) {
        return A((a + x.a) % P);
    }
};

Javaに慣れてしまうと引数や戻り値の扱いに頭を悩ますことはあまりありませんし、気を使わなくても安全に書けるのかもしれませんが。。。C言語をやり始めると辛いでしょうね。。。