目的
C++に関する備忘録として投稿します。随時更新予定。
外部から基底クラスのメンバ関数呼び出し
派生クラスインスタンスからこの基底クラスのfunc関数を呼び出すには「派生クラスインスタンス.基底クラス::メンバ関数();」という書き方をすることで解決する。
class Base
{
public:
// do something
virtual void func();
};
class Derived : public Base
{
public:
// do something
void func() override;
};
void Derived::func()
{
Base::func(); // これは基底クラスのfunc関数呼び出し
}
int main(void)
{
Derived d;
d.Base::Func(); // 基底クラスのfunc関数呼び出し
d.Func(); // 派生クラスのfunc関数呼び出し
}
インクルードの代わりに前方宣言を使う1
ある2つのヘッダーがお互いのクラスを参照している場合、それぞれがお互いのヘッダーをインクルードする際に気を付ける点がある。
#ifndef __HOGEA__
#define __HOGEA__
#include "hoge_b.hpp"
class HogeA
{
private:
HogeB* m_hoge_b; // HogeAクラスがHogeBクラスのポインタを保持している
public:
HogeA(){}
~HogeA(){}
};
#endif // __HOGEA__
#ifndef __HOGEB__
#define __HOGEB__
#include "hoge_a.hpp"
class HogeB
{
private:
HogeA* m_hoge_a; // HogeBクラスがHogeAクラスのポインタを保持している
public:
HogeB() {}
~HogeB() {}
};
#endif // __HOGEB__
上記のコードをコンパイルすると、以下のようにエラーとなる(VisualStudio2017でビルド時のエラー)。
エラー C2143 構文エラー: ';' が '*' の前にありません。 hoge_b.hpp 11
エラー C4430 型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません hoge_b.hpp 11
エラー C2238 ';' の前に無効なトークンがあります。 hoge_b.hpp 11
エラー C2143 構文エラー: ';' が '*' の前にありません。 hoge_b.hpp 11
エラー C4430 型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません hoge_b.hpp 11
エラー C2238 ';' の前に無効なトークンがあります。 hoge_b.hpp 11
コンパイル時の順序は次の通り。各手順においてどのファイルに注目しているのかを【 】で示している。
- 【hoge_a.hpp】#ifndefディレクティブでマクロ __HOGEA__が定義済みか判定する。未定義のためそれを#defineディレクティブで定義する
- 【hoge_a.hpp】hoge_b.hppをインクルードする。(この時点ではコンパイラはHogeAクラスを知らない)
- 【hoge_b.hpp】#ifndefディレクティブでマクロ __HOGEB__が定義済みか判定する。未定義のためそれを#defineディレクティブで定義する
- 【hoge_b.hpp】hoge_a.hppをインクルードする。
- 【hoge_a.hpp】#ifndefディレクティブでマクロ __HOGEA__が定義済みか判定する。これは定義済みのため、#endifまでのコードが無効化される。
- 【hoge_b.hpp】HogeBクラスをコンパイルする。この時、メンバ変数にHogeA*型があるが、コンパイラはHogeAクラスを知らないため、エラーとなる
このようにお互いのクラスを参照するようなケースは、例えばオブジェクト指向の概念の一つである多態性を取り入れる際に遭遇するだろう。
抽象クラスがいくつか定義されていて、それらの抽象クラス同士がお互いのクラスに登場している。
具体的には、以下の記事が参考になる。
デザインパターン ~State~
この記事の例では、ContextクラスとStateクラス(どちらも抽象クラス)がお互いのクラスを含んでいることが分かる。
この問題を解決するには、#includeの代わりに前方宣言を用いる。以下のようにコードを修正する。
//#include "hoge_b.hpp"
class HogeB;
//#include "hoge_a.hpp"
class HogeA;
前方宣言では、具体的なクラスの定義は知らないが、こんなクラス名のクラスが登場するぞ、ということをコンパイルに知らせる。これによってエラーを解決できた。
ところで、この例では__ポインタ型(HogeA* m_hoge_a; や HogeB* m_hoge_b;)__であるが、__クラス型(HogeA m_hoge_a; や HogeB m_hoge_b;)__の変数として定義した場合はどうだろう。以下のような結果となった。
エラー C2079 'HogeA::m_hoge_b' が 未定義の class 'HogeB' で使用しています。hoge_a.hpp 11
エラー C2079 'HogeA::m_hoge_b' が 未定義の class 'HogeB' で使用しています。hoge_a.hpp 11
あるヘッダーに定義されたクラスは、コンパイルされた時点でそのクラスの大きさを確定できる。しかし前方宣言では具体的なクラスのサイズが分からないため、コンパイルエラーとなる。ポインタ型の場合は(int* や double* や未知のHogeCであっても)4byteであるため、問題とならない。
「でもstd::vectorのような可変長のコンテナクラスは、自前のクラスに持たせるときにサイズを指定しなくても問題なくコンパイルできるし、実行時にサイズも変えられるけど?」と思うかもしれないが、これは__データの実体がどこにあるのか__を考えればよい。std::vectorやstd::stringのように動的にサイズが変わるものは、データはヒープ領域に置かれる。ここに置かれたものはポインタを介して参照される。つまり、クラス定義時に具体的な長さを持つ変数としてサイズを知っておく必要がない。
(以下に関しては後ほど追記予定)
*「自前のクラスにクラス型変数を定義したい場合はどうするの」
*「上記のような例に該当していてインクルードできないからクラスの前方宣言に修正したけど、そのクラスを継承するコードを書くとコンパイルエラーになる」
アップキャスト
C++ではメモリ管理をプログラマーが注意して行う必要がある。newして使い終わった後にdeleteしないとメモリリークを引き起こす。
極力、動的メモリ確保を使うことを避けて実装していきたい。
昔、仕事で初めてC++を使う時に、先輩がこんなコードを書いていた
try
{
ConfigLoader* cl = new ConfigLoader("config.xml");
}
catch(std::bad_alloc& e)
{
std::cerr << e.what() << std::endl;
}
if (!cl->validate())
{
// バリデーション失敗...
return false;
}
:
省略
:
delete cl;
省略の部分は、ConfigLoaderクラスが持つメンバ関数をいくつか呼び出すだけで、他の登場人物(クラス等)は存在しない。
C++を使ったことがなかった私は、そのコードを見て次のことをしなければならないのだと解釈した。
- クラスを使うにはnewする
- newに失敗する場合を考慮する
- 使い終わったらdeleteする
しかし、わざわざnewを使って書く必要はなかった。
ConfigLoader cl("config.xml");
if (!cl.validate())
{
// バリデーション失敗...
return false;
}
:
省略
:
これでnewもtry-catch文もdeleteもきれいさっぱりなくなった。
newnewしているところが多いため、カバレッジテストが面倒であったことを覚えている。
ちなみに10個上の先輩もC++を使ったことがない(それどころかチームメンバー全員がC++を知らない!)
さてここからが本題で、アップキャストのためにnewする必要はないかもしれないというお話。
上記の身の上話は「わざわざnewする必要がない」ということだが、アップキャストではポインタをキャストする。
「アップキャストするには、派生クラスをnewして基底クラスのポインタ型に代入(又は初期化)でいいんでしょ?」
と考えるかもしれない。以下に例を記載する。
Base基底クラスの抽象関数をDeserve派生クラスで実装して、それをClientのprint関数に渡してprintを呼び出すコード。
class Base
{
public:
Base() {}
virtual ~Base() {}
virtual void print() = 0;
};
#include "base.hpp"
#include <iostream>
#include <string>
class Deserve : public Base
{
private:
std::string m_str;
public:
explicit Deserve(const std::string& str) { m_str = str; }
~Deserve() {}
void print() override
{
std::cout << m_str << std::endl;
}
};
#include "base.hpp"
class Client
{
public:
Client() {};
~Client() {};
inline void print(Base* base) { base->print(); }
};
①定義時にアップキャスト
Client client;
Base* b = new Deserve("Hello"); // アップキャスト
client.print(b); // 同じBase*型を引数に渡す
delete b; // 使い終わったら忘れずにヒープ領域を解放
②引数に渡す時にアップキャスト
Client client;
Deserve* d = new Deserve("Hello");
client.print(d); // 型が違うがアップキャストされる
delete d; // 使い終わったら忘れずにヒープ領域を解放
③引数に渡す時にアップキャスト
#include <memory>
Client client;
std::shared_ptr<Deserve> d = std::make_shared<Deserve>("Hello");
client.print(d.get()); // Deserve*型で型が違うがアップキャストされる
④ローカル変数のアドレスを渡す
Client client;
Deserve d("Hello");
client.print(&d); // Deserve*型で型が違うがアップキャストされる
①と②はnew演算子を使った例。
③はshared_ptrを使って、動的に生成したインスタンスを管理する。deleteは、変数dがスコープを抜けたタイミングで勝手に解放してくれる。
④はローカル変数として定義された派生クラスのアドレスを渡している。これで、newもshared_ptrも使う必要がなくなった。
上記中に「使う必要がないかもしれない」と書いたのは、以下の可能性が考慮されるためである。
- 何らかの理由でスタックではなくヒープにデータを置く必要がある
- 呼び出す側のソースコードは変わらずクラス定義側でひっそりと変更された時に、予期せぬ関数呼び出しが行われる。
メモリ管理に関することは、リソース取得は初期化であるという考え方が大切。
参考Webページ/書籍
-
"前方宣言を使う"なんて記事を書いたが、Googleのコーディングスタイルではこの使用を避けるよう推奨している。 ↩