質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

4回答

21765閲覧

C++で複数のオブジェクト間で1つのオブジェクトを共有する方法

yama_da

総合スコア73

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

7クリップ

投稿2017/01/25 17:41

編集2017/01/26 11:27

こんにちは。C++のことで質問です。
例えば、

class C { public: Data _data; }; class B { public: C _objectC; Data _data; }; class A { public: B _objectB; Data _data; }; class Data {};

このように、AがBをメンバに持っていて、BがCをメンバに持っている状況で、DataというクラスのオブジェクトをA、B、Cそれぞれに共通して持たせたいのですが、(色々省略して書きます)

class C { public: Data _data; void setData(Data& d) { _data = d; } }; class B { public: C _objectC; Data _data; void setData(Data& d) { _data = d; _objectC.setData(d); } }; class A { public: B _objectB; Data _data; void init() { _data.addData(/*データを色々セット*/); _objectB.setData(_data); } }; class Data {};

のように、参照渡しで_dataを渡していくという方法はC++的にアリなのでしょうか?参照渡しを使ったのは、Dataクラスはなんらかのデータを保持したりその中かから検索したりするだけのクラスなので、中身の全く同じオブジェクトのコピーをわざわざ3つも作るより、1つのオブジェクトを共有したほうがメモリの節約になるんじゃないかと思ったからです。それとも、

class C { Data* _data; void setData(Data* d) { _data = d; } }; class B { public: C _objectC; Data* _data; void setData(Data* d) { _data = d; _objectC.setData(d); } }; class A { public: B _objectB; Data _data; void makeData() { _data.addData(/*データを色々セット*/); _objectB.setData(&_data); } }; class Data {};

のように、Aでオリジナル?のDataオブジェクトを作り、そのポインタをB,Cに渡していくという方法のほうが良いでしょうか?というより、ポインタを使うこと以外でこの2つのやり方に違いはあるのでしょうか?同じ値を参照しているという点ではどちらも同じに思えるのですが、、、イマイチ参照渡しが理解できていないのかもしれません。よろしくお願いします。

<2017/1/26 追記>
こんばんは。皆さん回答ありがとうございます。僕の例えが少し大雑把すぎたので、もう少し付け加えさしてください。
AはB、Cを管理する1つのおおきなクラスで、プログラムの最初から最後まで生きている?(すみません、言葉が見つかりません。。)寿命の長いオブジェクトです。AはメンバにBをいくつか持っていて、そしてBもCを複数持っている、そしてCはこの中で言うと最小単位のようなオブジェクトです。また、B,CはAが削除されるまで削除されないオブジェクトです。このABCのオブジェクト全体で共有したいデータがいくつかあり、それをどうやって共有しようかと思い質問しました。

例えば、クラスCの中にData型のオブジェクトdataを用意して、clsAobj.clsBobj.clsCobj.dataのようにすればアクセスできますよね(public指定を前提とする)?

majiponiさんのおっしゃった、この方法ももちろん考えたのですが、Cは複数あるので、内容の同じオブジェクトをいくつも持つのは無駄ではないかと思い、使いませんでした。
質問しておいてなんだという話になりますが、少しでもメモリが節約できればと思い参照を使おうとしたけれど、実際ポインタを使った方法と参照を使った方法でメモリの使用量に大きなさはあるのでしょうか?

オブジェクトの共有目的で参照を使った場合、寿命の管理が難しくなり、また、混乱しやすいと思っています。

raccyさんのおっしゃったように、自分があとで見て混乱しないようにするためにも、ひねくれて参照なんて使わずに、ポインタを使ったほうがまだ安全なのではないかと、回答を読んでいるうちに思いました。また、シングルトンというのもあるようですが、そこまでするほどの物でもないしなぁといった感じです。結局、どうするのがベストなのでしょうか?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

まず、書いているコードについてですが、関数の引数には参照渡しですが、メンバーは参照では無いため、結局コピーになります。つまり、共有はされていません

C++

1#include <iostream> 2 3class Data 4{ 5public: 6 int i = 0; 7 void addData(int x) 8 { 9 this->i += x; 10 } 11}; 12 13class C 14{ 15public: 16 Data _data; 17 void setData(Data &d) 18 { 19 _data = d; 20 } 21}; 22 23class B 24{ 25public: 26 C _objectC; 27 Data _data; 28 void setData(Data &d) 29 { 30 _data = d; 31 _objectC.setData(d); 32 } 33}; 34 35class A 36{ 37public: 38 B _objectB; 39 Data _data; 40 void init() 41 { 42 _data.addData(3); 43 _objectB.setData(_data); 44 } 45}; 46 47int main() 48{ 49 A a; 50 a.init(); 51 a._data.addData(2); 52 std::cout << "a._data.i = " << a._data.i << std::endl; 53 std::cout << "a._objectB._data.i = " << a._objectB._data.i << std::endl; 54 std::cout << "a._objectB._objectC._data.i = " 55 << a._objectB._objectC._data.i << std::endl; 56 return 0; 57}

上を実行して見ればわかるように、_dataメンバーが違う物になっています。Chironianさんの指摘通り、メンバーも参照にしないと共有してメモリ消費量を減らすという目的は達成できていません。(関数の引数を参照渡しにすること自体が無意味というわけではありません)

さて、メンバーも参照にした場合、気をつけなければいけないのは大本のオブジェクトの寿命です。大本のオブジェクトが削除されると、参照先がどうなっているか不定になります。

C++

1#include <iostream> 2 3class A 4{ 5public: 6 int &i; 7 A(int &i) : i(i) 8 { 9 } 10}; 11 12int main() 13{ 14 A *a; 15 { 16 int x = 42; 17 a = new A(x); 18 } 19 std::cout << a->i << std::endl; 20 delete a; 21 return 0; 22}

aが破棄される前にxは破棄されます。ブロックを抜けるとxが破棄されているため、xを参照しているa->iの値は不定です。環境によっては42のままの場合もありますが、paiza.ioで試す0になり、期待通りになりません。

ただし、これは参照では無くただのポインタにした場合も同じ事になります。破棄を防ぎながら、かつ、メモリ消費量を減らすとなるとスマートポインタを使うしかありません。
※ 下記コードはC++14以上で無いと動きません。

C++

1#include <iostream> 2#include <memory> 3 4class A 5{ 6public: 7 std::shared_ptr<int> i; 8 A(std::shared_ptr<int> i) : i(i) 9 { 10 } 11}; 12 13int main() 14{ 15 std::unique_ptr<A> a; 16 { 17 std::shared_ptr<int> x = std::make_shared<int>(42); 18 a = std::make_unique<A>(x); 19 } 20 std::cout << *(a->i.get()) << std::endl; 21 return 0; 22}

xはshared_ptrであるため、xが破棄されても、aの中のi(同じ先を見ているshare_ptr)はそのままあるので、その先はそのまま維持されます。そして、aが破棄されるときにi(の中身)も破棄されます。ただし、shared_ptrは参照カウント方式であるため、

  • ただのポインタの処理よりも僅かに遅い
  • 循環参照は自動的に破棄できない

という欠点がありますので、注意が必要です。


私個人の意見ですが、オブジェクトの共有目的で参照を使った場合、寿命の管理が難しくなり、また、混乱しやすいと思っています。むしろ、オブジェクトの寿命について意識しなければならないポインタを使った方がまだいいと思っています。私が書く時は、値渡しだとパフォーマンスが低下することを防ぐ目的以外では基本的に参照を使いません。


【追記】

アイデアの一つですが、親子関係を強調するなら、親のポインタを持たせるという手もあります。(コードは割と適当なので参考程度に)

C++

1#include <iostream> 2#include <memory> 3 4class Data; 5class A; 6class B; 7class C; 8 9class Data 10{ 11private: 12 int val; 13 14public: 15 Data(int val = 0) : val(val){}; 16 void addData(int x) 17 { 18 this->val += x; 19 } 20 int getVal() 21 { 22 return this->val; 23 } 24}; 25 26class A 27{ 28 friend B; 29 friend C; 30 31private: 32 Data _data; 33 std::unique_ptr<B> _objectB; 34 35public: 36 A(int val = 0) : _data(val), _objectB(std::make_unique<B>(this)) 37 { 38 } 39 Data &getData() 40 { 41 return this->_data; 42 } 43 B *getB() 44 { 45 return this->_objectB.get(); 46 } 47 void addData(int x) 48 { 49 this->_data.addData(x); 50 } 51}; 52 53class B 54{ 55 friend C; 56 57private: 58 A *const parent; 59 std::unique_ptr<C> _objectC; 60 61public: 62 B(A *parent) : parent(parent), _objectC(std::make_unique<C>(this)) 63 { 64 } 65 C *getC() 66 { 67 return this->_objectC.get(); 68 } 69 Data &getData() 70 { 71 return this->parent->_data; 72 } 73}; 74 75class C 76{ 77private: 78 B *const parent; 79 80public: 81 C(B *parent) : parent(parent) 82 { 83 } 84 Data &getData() 85 { 86 return this->parent->parent->_data; 87 } 88}; 89 90int main() 91{ 92 auto a = std::make_unique<A>(0); 93 a->addData(2); 94 std::cout << "a _data: " << a->getData().getVal() << std::endl; 95 std::cout << "a _objectB _data: " << a->getB()->getData().getVal() 96 << std::endl; 97 std::cout << "a _objectB _objectC _data: " 98 << a->getB()->getC()->getData().getVal() << std::endl; 99 return 0; 100}

親で共有させたいメンバーが増えていってもそれぞれの子でメンバーも宣言をする必要も無いですし、子が持つメンバーも親一人のポインタだけで済みます。ただ、親のコンストラクタでしか子が作れないので、ポインタにしないと駄目だったはず…。オブジェクト間で親子関係を持たせられるQtがそんな作りだったと思いました(Qtは自前で参照カウントとかしていたはずなので、もうちょっと複雑ですが)。

投稿2017/01/25 21:27

編集2017/01/26 18:37
raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Chironian

2017/01/26 03:42

> オブジェクトの共有目的で参照を使った場合、寿命の管理が難しくなり なるほど、はまりポイントの一つですね。 しかし、オブジェクトの依存関係をきちんと設計できていればありえない事態ではありますし、本質的な対策は依存関係を適切に設計することと思います。ここを手を抜くと後でひどい目にあいます。 その設計ミスの検出には有効と思います。 ただ、安易にshard_ptrを使うと依存関係の設計に手を抜いてしまい、負のスパイラルに陥ることはないでしょうか? 個人的には、shared_ptrは複数の管理主体を許す本質的に頭の痛い構造になるので、慎重に設計した結果shared_ptrがベストと判断出来た時に限定して使うものと考えています。
yama_da

2017/01/26 11:29

>上を実行して見ればわかるように、_dataメンバーが違う物になっています。Chironianさんの指摘通り、メンバーも参照にしないと共有してメモリ消費量を減らすという目的は達成できていません。 変数も参照を使わなければいけないんですね、知りませんでした。 追記したので、よろしければまた回答よろしくお願いします。
raccy

2017/01/26 17:34

> ただ、安易にshard_ptrを使うと依存関係の設計に手を抜いてしまい、負のスパイラルに陥ることはないでしょうか? そうですね。shared_ptrはGCのように完全ではありませんから、頼り切ると循環参照でメモリリークしまくっていたに陥りますからね。タイトルのテーマとして「複数のオブジェクトから」とあったので、親子関係とかがない場合を想定してあげてみました。
yama_da

2017/01/28 20:06

回答ありがとうございます。あれから少しして、データだけじゃなく関数やらなんやらも含めたくなったので、結局シングルトンでいってみることにしました。お世話になりました、また機会があればよろしくお願いします。
guest

0

こんにちは。

どちらも有りです。

なお、参照で渡せる時は参照で渡しておいた方がバグが発生しにくいです。
ポインタはnullptrにできますし、ポイント先を変更できます。
これらの操作が不要な時は参照を使うことでバグを避けやすくなります。


【追記】
ああ、でもよく見ると参照渡ししてますが、受け取り側はコピーで受け取ってますね。
それは勿体無いです。

C++

1class C 2{ 3public: 4 Data& _data; 5 6 C(Data& iData) : _data(iData) 7 { } 8}; 9 10class B 11{ 12public: 13 Data& _data; 14 C _objectC; 15 16 B(Data& iData) : _data(iData), _objectC(_data) 17 { } 18}; 19 20class A 21{ 22public: 23 Data _data; 24 B _objectB; 25 26 A() : _objectB(_data) 27 { 28 _data.addData(/*データを色々セット*/); 29 } 30};

参照はコンストラクト時にのみ設定できますので、初期化子を使って初期化する必要があります。

そして、上記の初期化の場合、B, Cのオブジェクトがコンストラクトされる時はまだ_dataはデフォルト・コンストラクタでのみ初期化されおり、addData()は呼ばれていません。
addData()関数ではなく、Dataクラスのデフォルト・コンストラクタでaddData()関数と同じ初期化をできると好ましいです。
もし、パラメータを渡す必要がある場合は、それなりに工夫する必要があります。


【追加の質問への回答】
依存関係をきっちり設計することは何れにせよ必須ですね。そして、きちんと設計されてますのできっちりプログラムすることが可能です。ですので、私なら参照を選択します。

ただ、もう少し知識が必要です。例えば、_objectBのデストラクタで_dataをアクセスするのであればその時まで_dataが生きている必要が有ります。_dataを_ojectBより先に定義するだけでそれを実現できますが、そのことを知っている人は少ないと思います。

C++11の文法と機能12.4 デストラクター(Destructors)より。

デストラクターの呼び出しは、コンストラクター呼び出しの逆順に行われる。コンストラクター呼び出しの順番については、12.6.2 基本クラスとデータメンバーの初期化を参照。
非staticデータメンバーの初期化の順番は、クラス定義の中でメンバーが宣言されている順番である。

そこで、深い知識を必要としないようなプログラミングをするのも1つの方針と思います。
shared_ptrを使えば、デストラクト時の細かいタイミングを考慮する必要を回避できます。
なお、Dataクラス内でAクラスをアクセスしたくなった時に、同じようにshard_ptrを使ってクラスAのインスタンスへのポインタをDataクラスで保持すると循環参照します。shard_ptrに頼って依存関係の設計に手を抜かなければ、大丈夫と思いますが。


【更に追記】
あっとと、シングルトンを使える状況(クラスAのインスタンスは1つしかない)であれば、単純に_dataをstaticメンバにすれば簡単です。別にシングルトン化する必要は特にありません。
みんな、A::_dataでアクセスできるようになります。
スケスケすぎて怖い場合は、_dataをprivateにしてBとCをAのfriendにすればOKです。

投稿2017/01/25 17:54

編集2017/01/26 12:36
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yama_da

2017/01/26 11:32

>参照はコンストラクト時にのみ設定できますので、初期化子を使って初期化する必要があります。 ご指摘ありがとうございます、知りませんでした。 追記したので、よろしければまた回答よろしくお願いします。
yama_da

2017/01/28 20:09

回答ありがとうございます。あれから少しして、データだけじゃなく関数やらなんやらも含めたくなったので、結局シングルトンでいってみることにしました(自分でやらないと言っていおきながらなんですが、、、)。お世話になりました、また機会があればよろしくお願いします。
guest

0

オブジェクト間に必ず所有関係があるなら、共有なんてしないのが一番だと思います。

…割と真面目な話、わざわざ共有させるメリットなんてあんまりないわけで、同じオブジェクトにアクセスする方法があればそれでよいのです。例えば、クラスCの中にData型のオブジェクトdataを用意して、clsAobj.clsBobj.clsCobj.dataのようにすればアクセスできますよね(public指定を前提とする)? この方法ならオーバーヘッドも0です。

…と書くと、先にコメントされたお二人から脳筋だと起こられそうなので補足。この方法の問題点は、やはりアクセス制御でしょう。不用意に内部オブジェクトを公開することになるので、不本意な使われ方に対して脆弱になります。アクセッサ、フレンド設定などをしても同様の問題は起こります。

提案のあった方法の中から選ぶのであれば、私は生のポインタを選びます。副作用の存在に一番シビアになれるのは、ポインタだと思うのです。(スマポは堅牢なのですが、個人的には余計な部分に意識が行ってしまうので…。)

ですが、ポインタで共有すべきだと私は言っていません。オブジェクトの値渡しや返り値で実現できるのにそれをしない、あるいはそもそも実現できないのは、設計に問題があることがほとんどです。多少便利、オーバーヘッドも少ないかもしれませんが、その共有、本当に必要ですか?

投稿2017/01/26 09:24

majiponi

総合スコア1720

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yama_da

2017/01/26 11:28

回答有り難うございます。追記しました、よろしければまた回答よろしくお願いします。
majiponi

2017/01/26 11:37 編集

グローバル変数(禁じ手)はどうですか? まあ、悪魔の囁きはさておき、変数をコンストラクトするときに参照なりポインタで要求するのって、グローバル変数と大して変わりないので、個人的にはあまりオススメしません。 使う必要があるときに一々メソッドの引数で参照を要求してもいいんですよ? そちらのほうが副作用とかはるかに分かりやすいですし。
yama_da

2017/01/28 20:01

回答ありがとうございます。あれから少しして、データだけじゃなく関数やらなんやらも含めたくなったので、結局シングルトンでいってみることにしました。お世話になりました、また機会があればよろしくお願いします。
guest

0

ベストアンサー

ポインタで共有して、寿命管理は、ref counterで行います。
親a, b, c で共有される Eを共有、a がEを使用し始めるときに、Eのref counter++, ~aするときにEのref counter--, ref counter == 0 なら親側からdelete.が基本戦略。

aのEをbでも使うときは、a->shareをbで使う。

class cse {
private:
int cnt ;
public:
cse () {cnt=1;}
cse * attach() {cnt++;return this;}
bool remove() {return --cnt;}
} ;
class csa {
private:
cse *ref ;
public:
csa () {ref = new cse() ;}
csa (cse * pe) {ref = pe->attach() ;}
cse * share() {return ref;}
~csa() {
if( ref->remove()) {delete ref;}
}
} ;
aでaのEを使うときは、循環する。防止策は、aがEを既に持っているときに、Eを付け変える動作を禁止する。
aのコンストラクタだけでEへの参照を変更できるようにすれば、自然に禁止できる。delete this できればコードはきれいになる。でもdelete this って安全かしら?

投稿2017/02/02 09:38

gm300

総合スコア580

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問