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

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

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

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

Q&A

5回答

5814閲覧

抽象コンストラクタ (続き)

mightyMask

総合スコア143

C++

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

0グッド

0クリップ

投稿2017/04/03 17:21

編集2017/04/03 21:19

抽象コンストラクタ
で質問させていただいた者です。

以下のようなコードで解決しようと思ったのですが、

c++

1#include <iostream> 2using namespace std; 3 4class Abstract { 5public: 6 Abstract() { initialize(); } 7 virtual void initialize() = 0; 8}; 9 10class Concrete : Abstract { 11public: 12 void initialize() override { 13 cout << "Concrete.initialize() が呼ばれた。" << endl; 14 } 15}; 16 17int main(void) { 18 Abstract obj = new Concrete(); 19}

「Abstract 抽象クラスをインスタンス化できません。」
「'初期化中' : 'Concrete*'から'Abstract'に変換できません。」
というエラーが出てしまいました。

c++を勉強し始めて1週間も経ってない本当に初心者なので、overrideキーワードの使い方や、newキーワードの使い方も微妙なのですが、何かおかしいがあれば教えてください。

#追記
main関数内で、Abstract obj;と記述するとスタック上にAbstractのインスタンスのための領域が確保される。
子クラスは親クラスより必要とする領域が大きい場合があるため、スタック上に確保された親クラス型の変数に子クラス型の変数を入れる事は絶対不可能。
そのためポリモーフフィズムを利用したい場合は、ヒープ領域上に子クラス型のインスタンスを生成し、そのアドレスを親クラスのポインタに格納する。
newキーワードは、ヒープ領域上にインスタンスを生成し、そのインスタンスのアドレスを返させる。
deleteキーワードは、ポインタが指すインスタンスを削除し、ヒープ領域から開放する。
そのため、newやdeleteキーワードはスタック上で管理しているインスタンスには使わない。
私が今回学べたのはこんな感じですが、合ってますか?

自分がやりたかった事は実は、インスタンスの生成に必要な引数の組み合わせが複数あり、それを子クラスに明示的に強制させたかったのです。
例えば、

c++

1class Abstract { 2public: 3 virtual Abstract(int, char) = 0; 4 virtual Abstract(bool, double) = 0; 5}; 6 7class Concrete : public Abstract { 8public: 9 Concrete(int i, char c) { } // ※ 10 Concrete(bool b, double d) { } // ※ 11};

こんな様にして、※印の行を書かないとエラーを出させるという事をしたいのですが。

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

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

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

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

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

guest

回答5

0

こんにちは。

c++を勉強し始めて1週間も経ってない本当に初心者

ということなら、抽象クラスを扱うのはちょっと早すぎるかも知れません。
現段階では純粋仮想関数は使わないことをお勧めします。
純粋仮想関数は使わないで済むことの方が多いですし。

mightyMaskさんがやりたいことは、動的ポリモーフィズム可能なクラス宣言とnewによる生成ではないでしょうか?

C++

1#include <iostream> 2using namespace std; 3 4class Abstract 5{ 6public: 7 Abstract() {} 8 virtual ~Abstract() {} 9}; 10 11class Concrete : public Abstract 12{ 13public: 14 Concrete() 15 { 16 cout << "Concrete() が呼ばれた。" << endl; 17 } 18 ~Concrete() { } 19}; 20 21int main(void) 22{ 23 Abstract* obj = new Concrete(); 24 delete obj; 25}

Concreteクラスを生成する際にConcreteのコンストラクタが呼ばれますのでここで初期化処理を行うことが一般的です。

上記のように基底クラスへのポインタをdeleteする際に、派生クラスのデストラクタが呼び出されるようにするためにデストラクタを仮想関数で実装することが強く推奨されます。

また、何か1つでも仮想関数があれば動的ポリモーフィズムできるようになりますので、デストラクタを仮想関数とすることで動的ポリモーフィズムできるようにしてみました。

投稿2017/04/03 18:53

編集2017/04/03 18:57
Chironian

総合スコア23272

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

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

0

追記2: 自分は適切な回答ができなかったのですが、他のみなさんのコメントを拝見して改めてC++でのオブジェクトの初期化メカニズムを確認してみました。

class 1 <- ... <- class i-1 <- class i <- class i+1 <- ... <- class N

のような継承関係があるクラスclass Nがあったとして、class Nのコンストラクションではコンストラクターの本体はclass 1 からclass Nまで、基底クラスから派生クラスの順番に起動されますが、class iのコンストラクターが実行中の状況ではclass 1 ~ class iまでの仮想関数の中からのみ起動関数が解決される仕様なんですね。

自分がこれを勘違いしたのは他の言語からの不完全な類推からです。Javaなどではコンストラクター本体の起動順序はC++と同様基底から派生の方向です。またclass iのコンストラクター実行中に起動するメソッドがどれになるかもC++と同様class 1 ~ class iの中から解決されます。しかし抽象メソッド(C++でいう純粋仮想関数)の扱いが異なっていました。C++ではvirtual/非virtualいずれでも必ずclass 1~class iの中で起動すべきメンバー関数が決定できなければならないようです。それに対してJavaではclass 1~class iの中までで解決できないメソッド(class iで抽象メソッドになっているもの)は起動が許され、それはclass i+1~class Nまでの中で解決されることになっています。

個人的な理解ですが、この仕様の違いはコンストラクターが起動される際のインスタンスの状態の違いによると思いました。C++ではclass iのコンストラクター本体の実行が開始する以前はそのインスタンスは「class iのインスタンスではまったくない」という思想であり、Javaはそれを若干ゆるくしており「必要に応じてclass iのインスタンスとみなせる」という思想だと思います。よって「class iのコンストラクターがclass 1~class iまでに実装がない抽象メソッドを呼び出すならば、それはclass iが派生クラスのメソッドを呼び出すことを明示的に意図していると解釈する」となっているのだと思います。

質問者さんがやろうとしていることはオブジェクト指向をサポートした言語の中で「それを許している言語」と「それが制限されている言語」に分かれる手法でであり、C++ではそれが制限されていると言えると思います。

こうした違いはいろいろありそうなので各言語の思想に応じて頭を切り替えなければなりませんね…


追記1: catsforepawさんとChironianさんの回答を拝見して気づきました。

自分の回答は抽象クラスのコンストラクターで純粋仮想関数を呼べないという点を指摘できていませんでした(できると思い込んでいました)。不適切な回答であったと思います。大変失礼しました。


クラス定義はよいと思うのですが、mainの中の記述が問題です。

Abstract obj;

とかくとC++の言語仕様ではこれだけで「実体が定義された」と見做します。つまりnewしなくてもオブジェクトの実体がここで生成されるとみなされ、コンストラクターが呼ばれるのです。正確にはmain関数のローカル変数用の領域であるスタック上にAbstractのインスタンスのための場所が確保され、main関数の実行が始まりこの変数宣言のところまで実行が進むとコンストラクターが自動的に呼び出されるという動きになります。(そしてmain関数の実行が終わってスコープから抜ける際に自動的にobjに対してデストラクターが呼ばれるという動きになります。Javaなどの言語に比べ非常に厳格なライフサイクル制御が可能になっているのですね)

さて、そういうわけでAbstractの実体がここに作られるという意味になるのですがAbstractは抽象クラスなのでコンパイルエラーになるということがおわりかと思います。また代入文の右辺はnew式ですがnew式の結果はConcreateクラスのインスタンスへのポインターになります。左辺はAbstractクラスのインスタンス実体です。ポインターを実体へ代入するためにはそれが可能となるようなAbstractクラスの代入演算子のオーバーロードなどが定義されていない限り「型不一致のため代入不可」となります。

多分質問者さんが意図したことはこのように書きます。

C++

1int main(void) { 2 Abstract *obj = new Concrete(); 3}

このようにするとobjはAbstractのインスタンスではなくインスタンスのアドレスを格納するポインター変数なのでこの変数を宣言しただけではAbstractクラスの実体が作られるということはありません。

代入文が実行される際に右辺が評価され、newによるConcreteのインスタンスが生成され、その先頭アドレスがobjに無事代入されるという意味・動きになります。

投稿2017/04/03 18:12

編集2017/04/04 08:48
KSwordOfHaste

総合スコア18394

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

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

0

これ、昔はまったことありますわ…

結論から言うと、基本クラスのコンストラクタで派生クラスの仮想関数は呼び出せません。理由は、基本クラスのコンストラクタは、派生クラスのコンストラクタより前に起動するからです。もし、その仮想関数が派生クラスの未初期化のオブジェクトを参照したら…
コンストラクタはまずvtblを設定し、それにより仮想関数は使えるようになります。今回、派生クラスのコンストラクタが呼ばれていないので、「派生クラスの」仮想関数にアクセスできないのです。
(ここまで当時の師匠の受け売り)

参考文献:
https://www.qoosky.io/techs/3fef7fa668

んで、どうすればお望みのことができるか、ですが…

無理です。派生クラスで基本クラスをどのように使用しようが、基本的に自由です。例えば、基本クラスは様々な用途のウインドウを作るために引数が多数必要だが、派生クラスではエディットボックスに特化しているため引数は少なくていい、というときに、派生クラスのコンストラクタが不要な引数を共用される仕組みになっていたら、非常に使い勝手が悪いです。

ポインタを利用した方法は、ファクトリ・メソッドのことですかね。いいアイデアだと思いますよ。←というよりも、このファクトリ関数の引数さえあれば、コンストラクタの引数強要って不要では?

個人的には、生のポインタよりスマートポインタの使用をオススメします。

参考文献:
http://naokirin.hatenablog.com/entry/20110124/1295846829

追記:遅延コンストラクタってキーワードをば。
さらに追記:誤字修正しました。

投稿2017/04/04 05:44

編集2017/04/04 15:44
majiponi

総合スコア1720

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

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

0

C++

1class Abstract { 2public: 3 Abstract() { initialize(); } 4 virtual void initialize() = 0; 5};

C++ではこのような書き方をしてはいけません。
仮想関数テーブルは、コンストラクタ実行時に設定されるのですが、設定するのは宣言中のクラス(上記コードでいえばAbstractクラス)の物なので、その時点では純粋仮想関数はまだ実体が定義されていません。したがって、上記コードのinitialize関数呼び出しは、実体のない関数への呼び出しとなり、実行時に例外が発生して強制終了するか、コンパイラーによってはリンクエラーとなります。

ちなみに、仮想関数テーブルはデストラクタ実行時も宣言中のクラスの物に設定されます(戻されます)。したがって、コンストラクタやデストラクタでは、純粋仮想関数に限らず仮想関数呼び出しは行うべきではありません。

投稿2017/04/04 02:25

編集2017/04/04 02:37
catsforepaw

総合スコア5938

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

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

0

#追記に対する回答

まず、追記された認識はあっています。
ただ肝心の何故失敗したかについては分かっていないような気がしますね。

最初のコードの誤りは2つあります。
一つは基本的にローカル変数として別の型を代入することはできないことです。
これは貴方も言っていますね。

× Abstract obj = new Concrete(); 〇 Abstract *obj = new Concrete();

2つ目は、コンストラクタ内で仮想関数を呼び出そうとしていることです。
コンストラクタ内ではまだ仮想関数テーブルにオーバライドされたメソッドのポインタが転写されていません。

超単純化した僕の理解だと、以下のようになっているので基底クラスのコンストラクタ内ではメソッドが未実装のままです。

new Derived() 1.Abstractのコンストラクタがよばれ、各変数の初期化が行われる 2.Derivedのコンストラクタがよばれ、各変数の初期化(仮想関数テーブル含む)が行われる

派生クラス内で仮想関数を利用したイニシャライザが実装できない理由が分かって頂けるでしょうか。
virtual指定の関数はメソッドへの参照をインスタンスの情報として持ち、派生クラスの初期化処理で入れ替えていっているだけですから、当然初期化中は呼び出すことはできません。

詳細な動作は違うかもしれないというか、そもそもコンパイラの実装依存なのでそれ以上の理解は不要と僕は考えています。
(最適化だなんだでそもそも仮想関数テーブルが用意されないこともあるし、どう動くか程度の理解に留めてます。速度を求める場合使わないのが最適解なのでユーザビリティ以上の理解が不要だったので。)


C++は 1週間でも他の言語触ったことある人ですよね?
Javaの質問がありますし、そう思って書きますね。

やりたいことで言っているコンストラクタの型を明示的に強制することは残念ながらできません。
そういう回答は過去にもあったと思います。
出来ません。

C++に数年間触れてないので現在の情報は分かりませんが、基本的に派生型がどのようなコンストラクタを持つか、直接制御する方法はないです。(Javaにそんなのありましたっけ…?)
なんかこういうことは他の言語でもあまりやらない気がします。

テンプレートを使ったファサードを受け皿とする、って形であれば、似たようなことはできるかもしれませんが…やるなんて聞いたことがないですね。

template<typename T> class MyType { T t; public : MyType(int i, char c) { t.Initialize(i, c); } MyType(bool b, double d) { t.Initialize(b, d); } } class InnerTypeBase { void Initialize(int i, char c) = 0; void Initialize(bool b, double d) = 0; }

たぶん別のアプローチを探す方が正解だと思います。

例えば、データと操作する関数を以下のようにわけてしまうとか。

class Data { public: Data(int i, char c) { /* ... */ } Data(bool b, double d) { /* ... */ } }; class Container { private: std::unique_ptr<Data> data; public: Container(std::unique_ptr<Data> &&data) : data(std::move(data)) { ; } };

考え方はStreamに対するStreamReaderクラスの実装に似ています。
複数の引数を必ずクラスに実装するのではなく、複数の引数の纏まりがなんであるのか、扱うものをちゃんとデータ型として明示しましょう。
そのあたりはC++でもC#でもJavaでも変わらないと思います。

データの操作方法が複数あるならコンテナをAbstractContainerにします。
これで派生形を通して複数種類の操作が実現できます。
コンストラクタは一つ実装しておくだけです。

反対にデータが複数種類あり、操作方法が一つなら、モデルの基底クラスを実装して渡すようにしましょう。

コンストラクタを経由する時点で仕様が複数クラスに渡って結合しすぎです。分けましょう。

// ちなみにunique_ptr除けばこんな感じの宣言ならできますよ。 Container c(Data(true, 0.5));

投稿2017/04/11 06:55

編集2017/04/11 07:03
haru666

総合スコア1591

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問