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

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

新規登録して質問してみよう
ただいま回答率
85.49%
Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

C++

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

Q&A

解決済

2回答

1137閲覧

二つの自作クラスのコンストラクタの実行順を逆にしたい

sakuramochi_py

総合スコア32

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

C++

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

0グッド

0クリップ

投稿2024/02/22 15:06

編集2024/02/22 15:08

実現したいこと

とあるクラスAを作り、クラスAを引数として初期化するクラスBを作成しました。
クラスA、Bそれぞれの定数を作成しようとしたところ、クラスBのコンストラクタが実行されてからクラスAのコンストラクタが実行されてしまいました。クラスAのコンストラクタが先に呼ばれるようにするか、根本の構造から見直して正しく動作するようにしたいです。

解決策でなくとも、ここが原因、悪手だという意見でも大変助かります。

該当のソースコード

問題点だけ残して簡素化したプログラムです。クラスA、Bの定義は以下4つのファイルで構成されています。

c++(A.h)

1#pragma once 2 3#include <iostream> 4 5class A 6{ 7public: 8 A(const int&); 9 const int checkNum; 10}; 11 12namespace A_VAR { 13 extern const A ONE, TWO; 14}

c++(A.cpp)

1#include "A.h" 2 3A::A(const int& num): checkNum(num) { 4 std::cout << "A constructor" << std::endl; 5} 6 7namespace A_VAR { 8 const A 9 ONE(1), 10 TWO(2); 11}

c++(B.h)

1#pragma once 2 3#include <iostream> 4#include "A.h" 5 6class B 7{ 8public: 9 B(const A&); 10 const int checkNum; 11}; 12 13namespace B_VAR { 14 extern const B ONE, TWO; 15}

c++(B.cpp)

1#include "B.h" 2 3B::B(const A& A): checkNum(A.checkNum) { 4 std::cout << "B constructor" << std::endl; 5} 6 7namespace B_VAR { 8 const B 9 ONE(A_VAR::ONE), 10 TWO(A_VAR::TWO); 11}

発生している問題・エラーメッセージ

main関数を実行すると以下のようになります。

c++(main.cpp)

1#include <iostream> 2 3#include "A.h" 4#include "B.h" 5 6int main() { 7 std::cout << "main function" << std::endl; 8 9 std::cout << A_VAR::TWO.checkNum << std::endl; 10 std::cout << B_VAR::TWO.checkNum << std::endl; 11 12}

txt(result.txt)

1B constructor 2B constructor 3A constructor 4A constructor 5main function 62 70

A_VAR::TWO.checkNumB_VAR::TWO.checkNumが同じ2になっていません。原因は、BコンストラクタがAコンストラクタより先に呼ばれていて、クラスAのメンバ変数checkNumに値が代入されていない状態でクラスBがその値を引き継いでしまっているからだと考えられます。

試したこと

メンバ変数の順番を変えてみたりしましたが手も足も出ませんでした。

補足情報(FW/ツールのバージョンなど)

Windows 11
Visual Studio 2022

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

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

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

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

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

hiroki-o

2024/02/22 19:37

提示されたソースを以下の3環境で試しましたが、現象が再現できませんでした。 ・Windows 11、Visual Studio 2022 ・FreeBSD 13.2、clang++ 14.0.5 ・Ubuntu 22.04.4、g++ 11.4.0 結果(3環境とも同じ) A constructor A constructor B constructor B constructor main function 2 2
dameo

2024/02/22 21:43

リンク順序の問題なのはすぐ分かるでしょう… #include <iostream> struct s { int value; s(int val):value(val){} }; extern const s a; const s b(a.value); const s a(1); // struct global_initializer { // s a, b; // global_initializer(): a(1), b(a.value) {} // }; // const global_initializer g; int main() { using namespace std; cout << "a=" << a.value << endl; cout << "b=" << b.value << endl; // cout << "g.a=" << g.a.value << endl; // cout << "g.b=" << g.b.value << endl; return 0; } コメント部分のようなイニシャライザを用意するのが一番簡単かも… 参照部分が全部変わるのでちょっと重い変更だけど
dameo

2024/02/22 22:34

インスタンスをmain以降で生成する前提で、初期化ルールを関数として渡す感じにした実装が以下 #include <iostream> struct s { int value; s(int val):value(val){} }; extern const s a; const s b(a.value); const s a(1); // struct s_owner { // typedef int (*initializer)(); // s* ps; // initializer f; // s_owner(initializer f): ps(nullptr), f(f) {} // const s& instance() { // if (ps == nullptr) ps = new s(f()); // return *ps; // } // }; // extern s_owner oa; // s_owner ob([]{return oa.instance().value;}); // s_owner oa([]{return 1;}); int main() { using namespace std; cout << "a=" << a.value << endl; cout << "b=" << b.value << endl; // cout << "oa.instance()=" << oa.instance().value << endl; // cout << "ob.instance()=" << ob.instance().value << endl; return 0; }
sakuramochi_py

2024/02/23 03:07

hiroki-oさん 3環境もテストしてくださりありがとうございます…!!私も質問のために新しくプロジェクトを作り、問題を再現しようとしたのですが問題は起きなかったです。問題が起きているプロジェクトをコピーして必要部分だけ残して削った感じなので、そのプロジェクト特有の問題なのかも知れません。そのプロジェクトでも途中までは問題なかったのですが、恐らくファイルを新しく追加した段階で順番が変わってしまったようです…
sakuramochi_py

2024/02/23 03:34

dameoさん イニシャライザの初期化される順番を利用した方法は盲点でした。定数をnamespaceではないですが代わりにクラスでまとめられるのもありがたいです。 2つ目のプログラムは結構複雑になりますね…なんとなくですが、初期化されていなかったら初期化するみたいな感じでしょうか。2つも解決策を提示して下さりありがとうございます。とても参考になります。
guest

回答2

0

ベストアンサー

SaitoAtsushiさんの書いてるとおりなんですが、個人的に問題点は定義順序依存であること自体だと思います。
つまりどのファイル(.cpp/.cxx)に記述するか、定義の順番で動作が変わることが悪手だと思うということです。

なので問題に対する素直な対応としては、SaitoAtsushiさんの言うように

  • ファイルの記述を固定(依存関係のあるものをまとめる)
  • ファイル内で定義の順番を固定(依存されるものを前に)

とする、のが一番簡単なんですが、ファイル内の定義の順番などに依存させず、ロジックを記述することで順番を強制する方法で考えてみました。以下は質問に対するコメントと重複しますが、少しだけ丁寧に書きました。

問題の更なる簡略化

まず複数ファイルであることがteratail的に面倒なので1ファイルで現象を再現させます。

C++

1#include <iostream> 2struct s { 3 int value; 4 s(int val):value(val){} 5}; 6extern const s a; 7const s b(a.value); 8const s a(1); 9int main() 10{ 11 using namespace std; 12 cout << "a=" << a.value << endl; 13 cout << "b=" << b.value << endl; 14 return 0; 15} 16// a=1 17// b=0

定義がa, bの順であれば、大丈夫なのですが、この例ではb, aの順になっているので、bが0になっているのが分かります。

対策

(1) グローバルな全インスタンスをまとめるクラスを定義する

c++

1#include <iostream> 2struct s { 3 int value; 4 s(int val):value(val){} 5}; 6struct global_initializer { 7 s a, b; 8 global_initializer(): a(1), b(a.value) {} 9}; 10const global_initializer g; 11int main() 12{ 13 using namespace std; 14 cout << "g.a=" << g.a.value << endl; 15 cout << "g.b=" << g.b.value << endl; 16 return 0; 17} 18// g.a=1 19// g.b=1

明示的に初期化子の順番でメンバの宣言順で初期化(Bearded-Ockhamさんの指摘により修正)されるので、心配の必要がなくなります。
また、後ろにロジックが記述出来るので、複雑な参照関係なども確実に記述できます。

(2) 初期値を関数にして渡し、生成を後回しにする

c++

1#include <iostream> 2struct s { 3 int value; 4 s(int val):value(val){} 5}; 6struct s_owner { 7 typedef int (*initializer)(); 8 s* ps; 9 initializer f; 10 s_owner(initializer f): ps(nullptr), f(f) {} 11 const s& instance() { 12 if (ps == nullptr) ps = new s(f()); 13 return *ps; 14 } 15}; 16extern s_owner oa; 17s_owner ob([]{return oa.instance().value;}); 18s_owner oa([]{return 1;}); 19int main() 20{ 21 using namespace std; 22 cout << "oa.instance()=" << oa.instance().value << endl; 23 cout << "ob.instance()=" << ob.instance().value << endl; 24 return 0; 25} 26// oa.instance()=1 27// ob.instance()=1

生成を後回しにすることで、起動コストを抑える効果もある対応です。関数にすることで、複雑なロジックを記述することが出来、参照するタイミングで生成されるので、順序を自由に変えられます。ただヒープを使うので組み込みとかだともう少し工夫がいるかもしれません。

(3) グローバル関数にして実体はローカルstatic

C++

1#include <iostream> 2struct s { 3 int value; 4 s(int val):value(val){} 5}; 6const s& a(); 7const s& b() { 8 static s* ps = nullptr; 9 if (ps == nullptr) ps = new s(a().value); 10 return *ps; 11} 12const s& a() { 13 static s instance(1); 14 return instance; 15} 16int main() 17{ 18 using namespace std; 19 cout << "a()=" << a().value << endl; 20 cout << "b()=" << b().value << endl; 21 return 0; 22} 23// a()=1 24// b()=1

似たような方法ですが、Cとかでも昔からあるグローバル関数にして実体はローカルにstaticで持つ方法などがあります。インスタンス生成の実装が散らばる傾向はあるかもしれませんが、単純で分かりやすいのがいいところ。

投稿2024/02/23 03:39

編集2024/02/27 19:46
dameo

総合スコア943

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

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

sakuramochi_py

2024/02/23 15:46

返信遅くなり申し訳ありません。 3つの方法を解説や利点を交えながら説明してくださっていてとても分かりやすく、私では思いつかなかった意外な手法が使われていて参考になります! nullptrを用いる方法から思いついたのですが、とりあえず定義順依存で、逆順になっていたらエラーを吐いて強制終了させるような方法もあると思います。質問のソースコードを以下のように少し変えて、 class A { public: A(int); const int checkNum; const bool constructorCalled = false; }; A::A(int num): checkNum(num), constructorCalled(true) { std::cout << "A constructor" << std::endl; } B::B(A A): checkNum(A.checkNum) { if (!A.constructorCalled) { std::cout << "Error: B constructor called before A constructor" << std::endl; exit(EXIT_FAILURE); } std::cout << "B constructor" << std::endl; } このようにすれば将来的に何らかの理由で逆順になってしまっていてもすぐに気づけると思います。 大変勉強になります。お時間割いて回答していただき本当にありがとうございます…!
dameo

2024/02/23 16:26

flagなどの状態管理はバグりやすいところでもあります。 なので、安易にメンバ変数に入れると「このフラグなんだっけ?」みたいな複雑度を増す副作用があるかもしれません。 またコンパイルエラーになれば分かりやすいのですが、実行時のチェックになっていることがやや気になります。 ただ、この辺は私の知る限りどうにもならないことも多く、実際のコードを見ていない段階では「こうするのが良さげ」というご提案も出来ません。 また観点としては別角度から、スレッドや割込みなどの同期処理がある場合もあり、簡単な処理ならアトミックな読み書きが出来るけど、後ろで処理するとそれも保証できないので、(2)(3)のようにあまりロジックを入れたくないというような話もあるかと思います。 あくまで、いろいろ簡略化した話であることを念頭に置いて頂き、実際のコードについては個々に十分ご検討頂いてからご自身で最善と思う記述をお願い致します。
sakuramochi_py

2024/02/23 17:11

コメントなどでフラグの役割などを記述しておくのが限界でしょうか。 実際のコードは、今回のONE,TWOなどの定数が7個ほどある状態なので、変数として(2)(3)の書き方だと少しプログラムを助長させてしまうのではと考えています。なので宣言がまとまっていて簡潔に書ける(1)が自分の場合には最善だと思い、(1)を採用させて頂きました。夜遅くまでお付き合いいただきありがとうございました!
dameo

2024/02/23 17:29

> コメントなどでフラグの役割などを記述しておくのが限界でしょうか 成果物がどのようなものかも分からないので、その流儀に従って頂ければと思います。 > (1)を採用させて頂きました ちゃんと伝わってるようだし、ご自身で決められるものであれば、特に理由付けは必要なく、気分や好みで決めていいと思います。 さきほどコメントを返したのはコードの提示があり、意見を求められた気がしたからなだけなので、無理に返さなくても大丈夫です。
Bearded-Ockham

2024/02/27 19:36

> 初期化子の順番で初期化される ではなく、メンバ変数の宣言された順番ですね。
dameo

2024/02/27 19:49

今の今まで知りませんでした。今まで無意識のうちに初期化子順で並べてたかも。 修正しておきましたが、重要な誤りなので、質問者さんはこれでいいのか分かりません。
guest

0

異なる翻訳単位にある静的変数はどちらが先に初期化されるか規定されていません。 直接には制御する方法もありません。

最も簡単な解決方法は同じ翻訳単位内で定義することです。

投稿2024/02/23 00:10

SaitoAtsushi

総合スコア5444

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

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

sakuramochi_py

2024/02/23 13:31

同じファイルで実装すれば上から実行されて順番が固定になるというシンプルなことですが思いつきませんでした… ファイルを役割ごとに分けるということに固執しすぎていたことに気づけました。回答ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.49%

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

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

質問する

関連した質問