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

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

ただいまの
回答率

90.23%

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

受付中

回答 5

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,287

mightyMask

score 101

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

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

#include <iostream>
using namespace std;

class Abstract {
public:
    Abstract() { initialize(); }
    virtual void initialize() = 0;
};

class Concrete : Abstract {
public:
    void initialize() override {
        cout << "Concrete.initialize() が呼ばれた。" << endl;
    }
};

int main(void) {
    Abstract obj = new Concrete();
}


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

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

追記

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

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

class Abstract {
public:
    virtual Abstract(int, char) = 0;
    virtual Abstract(bool, double) = 0;
};

class Concrete : public Abstract {
public:
    Concrete(int i, char c) { }     //  ※
    Concrete(bool b, double d) { }  //  ※
};

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

+3

追記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クラスの代入演算子のオーバーロードなどが定義されていない限り「型不一致のため代入不可」となります。

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

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

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+3

こんにちは。

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

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

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

#include <iostream>
using namespace std;

class Abstract
{
public:
    Abstract() {}
    virtual ~Abstract() {}
};

class Concrete : public Abstract
{
public:
    Concrete()
    {
        cout << "Concrete() が呼ばれた。" << endl;
    }
    ~Concrete() { }
};

int main(void)
{
    Abstract* obj = new Concrete();
    delete obj;
}


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

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

class Abstract {
public:
    Abstract() { initialize(); }
    virtual void initialize() = 0;
};


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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

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

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

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

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

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

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

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

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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));

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 90.23%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる