最近のコメント

最近のトラックバック

ウェブページ

2017年10月
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        

他のアカウント

無料ブログはココログ

« 2016年10月 | トップページ | 2016年12月 »

2016年11月

2016年11月19日 (土)

C++ でのconst性ってどこまで保証されてる?

大昔C言語を少しかじったことがある。

C++は自分でメモリ管理をするのはちょっときついと、勉強しなかった。ただし、CとC++の非互換性の部分はチェックしたが。

今でも、特にC++でプログラムする必要はないのだが、教養として少しずつ勉強をしている(かたつむりのように鈍く)。

C++ では const はとても大事だ。
しかし、const宣言では何が保証されるのだろうか?

この疑問はスマートポインタの一種のshard_ptr の動きから生まれた。

クラスの中で

const std::shared_ptr<T> p;

と「定数」pをつくっても、p.use_count() の値は変化しえる。というか変化できないと困る。

long getUseCount() const { return p.use_count();}

というconstメンバ関数を作ることはできるが、この値は変化し得るわけだ。
これは大丈夫なのか?といろいろ検索した結果、constメンバ関数が保証するのは const *this である、と覚えればいいと分かった。
つまり、クラスの中の変数の変化がないだけであって、ポインタ変数の参照先は書き換えることができる。(またstatic変数も書き換えてよい)。

shard_ptr も参照カウンタはおそらくポインタを使って共有されているから書き換え可能で問題はない。use_count() は変化してよい。これで納得。

・・・が、それは本当か? 例えば、const 参照引数されたクラスのインスタンスで非スタティック/非ポインター・メンバー変数が書き換えられたりしないのか?mutable とか const_cast など使わずに。

ちょいと実験してみた。

Foo.h
----------------

#ifndef FOO_H_
#define FOO_H_

// コピーコンストラクタの引数のconst性を破壊できるか?をテストするクラス。
class Foo{
private:
int counter_ = 0; //コピー・ムーブの元の場合、自分がコピーされた回数を数える
// つもりで最初は設定した。
// しかし、コピー・ムーブの仕方によってはリセットされるなどがあり得るので
// この変数が正確には何を数えているのか作者にもよく分からない。

int* pcounter_ = &counter_;
// 初期状態ではcounter_の位置。書き換えられ得るので注意


public:
Foo(){}
Foo( const Foo& f){
counter_ = f.counter_;

pcounter_ = f.pcounter_;
// this->pcounter_ は f.pcountrのコピーなので
// this->counter_ を参照しない。
// f.pcounter_ も書き換えられてる可能性があるので静的にpcounter_の参照先は決定できない
// *this が生きているうちにfの寿命が尽きたりなどすると簡単にダングリングポインタに
// なるので非常に危険

++*pcounter_;
// f.pcounter_ が f.counter_ を参照している場合はf.counter_ を1増加させる。
// int 型の f.counter_ を書き換えるとするとconst性を壊す可能性がある。
// そうでない場合も、どれかのFooインスタンスのcouter_変数を増加させる。
// ただし、pcounter_ がダングリングポインタになってる場合は何が起きるか分からない。
// 以下のムーブコンストラクタや=のオーバーロードなどでも同じ問題がある。
}
Foo( Foo&& f){
// ちょっとムーブっぽく、f.pcounter_ が消えても大丈夫なようにしておく。大した意味はない。
counter_ = *f.pcounter_;
f.pcounter_ = nullptr;
pcounter_ = &counter_;
++counter_;
}
Foo& operator=(const Foo& f){
counter_ = f.counter_;
pcounter_  = f.pcounter_;
++*pcounter_;
return *this;
}
Foo operator=(Foo&& f){
// ちょっとムーブっぽく、f.pcunter_ が消えても大丈夫なようにしておく。大した意味はない。
counter_ = *f.pcounter_;
f.pcounter_ = nullptr;
pcounter_ = &counter_;
++*pcounter_;
return *this;
}
virtual ~Foo(){}

// ゲッターメソッド
const int& counter() const {return counter_;}
int deref_pcounter() const {return *pcounter_;}

};

#endif // FOO_H_

--------

pcounter_ ポインタメンバ変数がメンバ変数 counter_ を参照している。こういうことをするといろいろ危険である。

テスト・プログラム

Foo.cpp
----------------

#include <iostream>

#include "foo.h"

Foo idFoo( Foo x){ return x;}

int main(){
Foo a;
std::cout << "Foo a;" << std::endl;
std::cout << "a.counter()="<< a.counter() << std::endl;
std::cout << "a.deref_pcounter()="<< a.deref_pcounter() << std::endl;

Foo b = a; // 初期化
std::cout << std::endl << "Foo b = a;" <<std::endl;
std::cout << "a.counter()=" << a.counter() << std::endl;
std::cout << "a.deref_pcounter()="<< a.deref_pcounter() << std::endl;
std::cout << "b.counter()=" << b.counter() << std::endl;
std::cout << "b.deref_pcounter()="<< b.deref_pcounter() << std::endl;

Foo c;
c = b; // 代入
std::cout << std::endl << "Foo c; c=b;" << std::endl;
std::cout << "a.counter()=" << a.counter() << std::endl;
std::cout << "a.deref_pcounter()="<< a.deref_pcounter() << std::endl;
std::cout << "b.counter()=" << b.counter() << std::endl;
std::cout << "b.deref_pcounter()="<< b.deref_pcounter() << std::endl;
std::cout << "c.counter()=" << c.counter() << std::endl;
std::cout << "c.deref_pcounter()="<< c.deref_pcounter() << std::endl;

Foo d;
c = a; // c の書き換え
a = d; // a の書き換え
std::cout << std::endl << "Foo d;c = a; a = d;" << std::endl;
std::cout << "a.counter()=" << a.counter() << std::endl;
std::cout << "a.deref_pcounter()="<< a.deref_pcounter() << std::endl;
std::cout << "b.counter()=" << b.counter() << std::endl;
std::cout << "b.deref_pcounter()="<< b.deref_pcounter() << std::endl;
std::cout << "c.counter()=" << c.counter() << std::endl;
std::cout << "c.deref_pcounter()="<< c.deref_pcounter() << std::endl;
std::cout << "d.counter()=" << d.counter() << std::endl;
std::cout << "d.deref_pcounter()="<< d.deref_pcounter() << std::endl;

Foo e = idFoo(a);
std::cout << std::endl << "Foo e = idFoo(a);" << std::endl;
std::cout << "a.counter()=" << a.counter() << std::endl;
std::cout << "a.deref_pcounter()="<< a.deref_pcounter() << std::endl;
std::cout << "e.counter()=" << e.counter() << std::endl;
std::cout << "e.deref_pcounter()="<< e.deref_pcounter() << std::endl;



return 0;
}

--------

実行結果:

--------
Foo a;
a.counter()=0
a.deref_pcounter()=0

Foo b = a;
a.counter()=1
a.deref_pcounter()=1
b.counter()=0
b.deref_pcounter()=1

Foo c; c=b;
a.counter()=2
a.deref_pcounter()=2
b.counter()=0
b.deref_pcounter()=2
c.counter()=0
c.deref_pcounter()=2

Foo d;c = a; a = d;
a.counter()=0
a.deref_pcounter()=1
b.counter()=0
b.deref_pcounter()=0
c.counter()=2
c.deref_pcounter()=0
d.counter()=1
d.deref_pcounter()=1

Foo e = idFoo(a);
a.counter()=0
a.deref_pcounter()=2
e.counter()=3
e.deref_pcounter()=3
--------

わけの分からない結果が出たが、どうやらFooクラスのインスタンスの int型変数 counter_ はコピーコンストラクタのコピー元で const が付いていても変化するようだ。

idFoo() 関数は値渡しなので、コピー回数が増えたようだ。

・・・まあ、C++もC言語の流れを汲んでるから、仕方ないな。

追記: ムーブコンストラクタ等、少々書き換えました。ダングリングポインタが出る可能性は全然なくなってませんが。

追記2: C++に詳しい人々に聞いたら、単に未定義動作をするプログラムを作っただけ、というだけの話のようです。

« 2016年10月 | トップページ | 2016年12月 »