簡単に Pimpl を使ってみる
名前は知っていたけど、使ったことがなかったので、試しに使ってみた。
Pimpl とは
にきび(pimple)ではありません。Pointer to Implementation
(実装へのポインタ)を略して、Pimpl
です。
このテクニックによって、プライベートな詳細を開示せずに済みます。つまりインタフェースと実装を切り離す方法です。
これは、実際にコードを見たほうが早いと思うので、先にコードを出します。
実装
とりあえず以下のようなコードを書いてみました。これは、まだ Pimpl を使っていません。
// Widget.h #pragma once #include <stdint.h> class Widget { public: Widget(); ~Widget(); int32_t getNum(); private: int32_t num_ = 32; }; // Widget.cpp #include "Widget.h" #include <iostream> Widget::Widget() { std::cout << "Constructor" << std::endl; } Widget::~Widget() { std::cout << "Destructor" << std::endl; } int32_t Widget::getNum() { return num_; }
Widget::getNum() を呼ぶことで、内部の num のデータを取れるクラスです。このクラスを使う側(ユーザ)は、データの取り方が分かればいいので、変数 num があることを知らせる必要はありません。なので、これを実装ファイル(Widget.cpp
)に入れてやります。
// Widget.h #pragma once #include <stdint.h> class Widget { public: Widget(); ~Widget(); int32_t getNum(); private: class Impl; Impl* impl_; }; // Widget.cpp #include "Widget.h" #include <iostream> class Widget::Impl { public: Impl() { std::cout << "Impl Constructor" << std::endl; } ~Impl() { std::cout << "Impl Destructor" << std::endl; } int32_t num = 32; }; Widget::Widget() : impl_(new Impl()) { std::cout << "Constructor" << std::endl; } Widget::~Widget() { std::cout << "Destructor" << std::endl; delete impl_; impl_ = nullptr; } int32_t Widget::getNum() { return impl_->num; }
まずヘッダから見ていきます。private
にメンバ変数 num
がありましたが、それが消えて、代わりに クラス Impl
の前方宣言とそのクラス Impl のポインタ
を追加しました。
次に実装側ですが、初めに クラス Imple
の定義を行っています。その中にさっきは、Widget.h
にあったメンバ変数が Widget::Impl
に移動しています。
Widget のコンストラクタ
の初期化子リストに impl_
が追加され、デストラクタでは、impl_ の開放
を行っています。最後に、Widget::getNum() は、impl_のメンバ変数 num をアクセスして、値を返すように変更されています。
このように、詳細を実装ファイル内に隠してしまうのが、Pimpl です。
Pimpl を実装するときの注意
Pimpl を書くときに、いくつか注意することがあります(C++ に慣れている方は大したことではないですが)。
まず、Pimpl となる変数(今回だと impl_
)の型は、ポインタ(Impl*
)でないといけません。当たり前ですが、ヘッダ側は、Impl
の実装内容を知らないので、Impl
のオブジェクトを持てません。
Impl の詳細を書くときは、必ず Impl を利用する処理よりも先に書くようにします。今回だと Widget::Widget()
よりも先に書く必要があります。これも Impl
の詳細を知らないと使えないためです。
最後に、デストラクタでは必ず Impl の変数を開放するようにします。ポインタなので、当然ですね。
Pimpl をスマートポインタにする
Pimpl をスマートポインタに変更することで、デストラクタ内で Pimpl を開放処理を書かなくて済みます。例えば以下のようにします。
std::unique_ptr<Impl> impl_;
デメリット
Pimpl を使うことでいくつかデメリットがあります。
- コンストラクタでオブジェクトの割当、デストラクタでオブジェクトを破棄する必要がある
- すべてのプライベートメンバは、Pimpl を通じてアクセスしないといけない
- Pimpl をプライベートにすると、Pimpl を持つクラス(Widget)しか Pimpl のメンバにアクセスできない
- プライベート仮想メソッドを作れない。オーバライドする場合、パブリックにして、派生クラスがオーバライドできるようにしないといけない
- Pimpl(impl_) から、それを持つクラス(Widget)のパブリックなメンバ変数にアクセスするには、Pimpl にそのクラスへのポインタを追加するか、そのメソッドにクラスを渡す必要がある
- const メソッド内で Pimpl(impl_)のメンバ変数を変更してもコンパイラは検知することができない
いくつかデメリットはありますが、実装する側のデメリットであり、利用者に対してのデメリットはないので、使わない理由にはならないでしょう。
メリット
次は、メリットです。
- ユーザ側が見ることになるヘッダファイルの内容がすっきりする。
- 実装の詳細をユーザ側に見られることがなくなり、不正な方法でアクセスすることもできなくなる。
- ヘッダのインクルードファイルが減るので、結合度が減る。
- コンパイル時間が高速化される
- バイナリ互換性が向上する
- Pimpl は要求時に作ることもできるので、ネットワーク接続など制限のあるリソースやコストの高いリソースの割当のときに役に立つ。
などなどあります。詳細は、参考にした本を見てください(最後に載せています)
コピーセマンティクス
Pimpl を持つクラスをコピーした場合、当然 Pimpl もコピーされる。この状態で、コピー元か、コピー先のオブジェクトを開放したとき、もう片方のオブジェクトが Pimpl にアクセスすると Pimpl はすでに開放されているため、エラーが発生します。
これを防ぐためには、3つの方法があります。
個人的には、スマートポインタが楽だと思います。コピーを禁止したい場合は、std::unique_ptr
, コピーを許可するなら std::shared_ptr
でいいと思います。
スマートポインタを使えば、コピーセマンティクスを書く必要もなくなります。
おわり
Pimpl は、有効に使えると強力な反面、注意する点が多い方法だと感じました。デメリットは小さいので、積極的に使っていきたいですが、プライベートをなんでもかんでも Pimpl に実装するのも違うと思うので、よく考えて使いたいと思います。
Pimpl の書き方をメインに書いたので、どう活用するかは分かりにくかったかもしれません。そういった内容は、参考にした本に載っているのでそちらを参照するといいでしょう。
今回のコードは、以下に上げています。
参考にした資料です。
- 作者: マーティン・レディ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/11/15
- メディア: Kindle版
- この商品を含むブログを見る
拡張式 Factory Method
ナッツ食べてます。そこそこおいしいです。
前回、FactoryMethod
を作りましたが、今回は、それを改造します。
pickles-ochazuke.hatenablog.com
基本的な FactoryMethod だと、新しいクラスを作ったときに毎度 FactoryMethod 側も新しいクラス用の処理を追加しないといけません。そこで、新しいクラスへの対応を実行中に行えるようにします。
概要
大まかに書くと、抽象基本クラスを継承した新しいクラス側に、"自身のオブジェクトを作成し、それを返す" コールバック関数を作ります。そのコールバック関数と key
となる文字列を FactoryMethod に渡し、FactoryMethod 側は、その関係を覚えておきます。実際にオブジェクトが必要になったときは、key
となる文字列を渡すことで、それに対応したコールバック関数が FactoryMethod 内で呼ばれ、作成されたオブジェクトが返ってくる。という感じです。
図にすると以下のような感じでしょうか。
上記は、Callback の登録のみしか書いていませんが、削除もできたほうがいいので、実装時には削除用の関数も作ります。
FactoryClass
まずシーケンス図にあった FactoryClass
のヘッダは以下のとおりです。
クラス名は、前回の続きなので SlimeFactory
となっています。
また、SlimeFactory で作られるオブジェクトは、すべて抽象基本クラスの ISlime
を継承しています。
#pragma once #include <string> #include <map> #include <functional> #include "ISlime.h" class SlimeFactory { public: using DoMakeCallback = std::function<ISlime*()>; SlimeFactory() = delete; ~SlimeFactory() = delete; static void RegisterSlime(const std::string& type, DoMakeCallback cb); static void UnregisterSlime(const std::string& type); static ISlime* DoMakeSlime(const std::string& type); private: using CallbackMap = std::map<std::string, DoMakeCallback>; static CallbackMap DoMakes_; };
上から順番に見ていきます。まず、using DoMakeCallback = std::function<ISlime*()>
ですが、
これは、コールバック関数の型を定義しています。using DoMakeCallback
は書くのを楽にしているだけで、std::function<ISlime*()>
が大事です。これが、登録するコールバック関数の型になります。登録したいクラスは、コールバック関数を作るとき、この型(ISlime*()
)を守る必要があります。std::function
は、関数ポインタ用のクラスだと思っておけば問題ないです。
コンストラクタとデストラクタは、今回必要ないので delete
しています。実際に FactoryClass を作るときは、1つしか作られないようにシングルトン
にするといいでしょう。
static void RegisterSlime(const std::string& type, DoMakeCallback cb)
は、コールバック関数を登録するための関数です。登録したいコールバック関数があるとき、この関数を呼び出して、引数に key
となる文字列とコールバック関数
を渡してやります。
static void UnregisterSlime(const std::string& type)
は、先ほどとは逆で、登録済みのコールバック関数を削除する関数です。使うときは、登録したときに渡した文字列と同じ文字列を引数に渡すだけです。
static ISlime* DoMakeSlime(const std::string& type)
は、オブジェクトが必要になったときに呼びます。引数に登録時の文字列を渡してやることで、生成されたオブジェクトが返ってきます。
using CallbackMap = std::map<std::string, DoMakeCallback>
は、これも型を定義しているだけです。std::map<std::string, DoMakeCallback>
は、コールバック関数を登録するための型になります。std::string
が key
となり、DoMakeCallback
が登録されたコールバック関数
です。
最後に、static CallbackMap DoMakes_
ですが、これは、コールバック関数が登録される場所になります。
次に、ソースファイルを見ていきます。
#include "HealSlime.h" #include <iostream> // インスタンス化 SlimeFactory::CallbackMap SlimeFactory::DoMakes_; void SlimeFactory::RegisterSlime(const std::string& type, DoMakeCallback cb) { DoMakes_[type] = cb; } void SlimeFactory::UnregisterSlime(const std::string& type) { DoMakes_.erase(type); } ISlime* SlimeFactory::DoMakeSlime(const std::string& type) { if (type == "Slime") { return new Slime(); } else if (type == "SheSlime") { return new SheSlime(); } else if (type == "HealSlime") { return new HealSlime(); } CallbackMap::iterator it = DoMakes_.find(type); if (it != DoMakes_.end()) { if (!it->second) { std::cout << "呼び出せない" << std::endl; return nullptr; } return (it->second)(); } return nullptr; }
上から順番に見ていきましょう。
まずは、SlimeFactory::CallbackMap SlimeFactory::DoMakes_
で static の変数をインスタンス化してやります。
RegisterSlime(const std::string& type, DoMakeCallback cb)
は、登録なので、登録用の変数 DoMakes_
にコールバック関数を登録しています。
UnregisterSlime(const std::string& type)
は、削除なので、type
を元に DoMakes_
に登録されているコールバック関数を削除しています。
DoMakeSlime(const std::string& type)
は、type
を元に作成すべきオブジェクトを決め、作成し、そのオブジェクトを返します。
Slime
, SheSlime
, HealSlime
は、前回作成した内容です。そこを抜けると type
と関連するコールバック関数があるか探します(CallbackMap::iterator it = DoMakes_.find(type);
)。見つかれば、そのコールバック関数が呼べるか確認(if (it != DoMakes_.end())
)し、問題なければ、呼び出して、返ってきたオブジェクトをそのまま返します(return (it->second)()
)。
以上が拡張式の FactoryMethod になります。
登録するためのコールバック関数の中身は、以下のように書きます。
static ISlime * DoMakeMyself() { return new MySlime(); }
このメソッドを RegisterSlime
の第二引数に渡してやれば、DoMakes_
に登録されます。
実行
実際に使うときは、以下のように使います。
std::function<ISlime*()> func = MySlime::DoMakeMyself; SlimeFactory::RegisterSlime("MySlime", func); ISlime* slime = SlimeFactory::DoMakeSlime("MySlime");
結果は、以下です。
ぼくが考えた最強のスライム が現れた! 続行するには何かキーを押してください . . .
以上のように、新しいクラスを作ったときは、FactoryClass 側で何も追加する必要なく、対応することが出来ました。
おわり
実際に作ってみると、することは難しそうだけど、作ってみたら簡単だった。ように感じました。コールバック関数の登録も削除も一行で済み、呼び出すときもfind
で探して、呼び出すだけですので。
今回の難しいところは、拡張式の FactoryMethod の作り方よりも、関数ポインタ、std::map、を理解しているかどうかだと思います。さらに言ってしまえば、std::map は今回採用しているだけなので、何か key を渡されたときに、それに関係する関数ポインタを返せるならどう作ってもいいわけです。なので、今回一番重要なのは、関数ポインタを理解しているかどうかなのだと思いました。
最後に、コードと参考資料を載せておきます。
- 作者: マーティン・レディ,Martin Reddy,三宅陽一郎,ホジソンますみ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2012/10/31
- メディア: 単行本
- 購入: 4人 クリック: 106回
- この商品を含むブログ (11件) を見る
Factory Method を使って、スライム属を作る
Factroy Method
をドラクエのスライムで表現してみる記事。
Factory Method とは
オブジェクト指向における再利用のためのデザインパターンの一つです。そのデザインパターンの中でも生成に関するパターン
の一つ。
このパターンによって、オブジェクト型を指定せずにオブジェクトを生成できるようになります。
簡単に説明すると、関数内でコンストラクタを呼び、生成したオブジェクトを返すのがこのパターンの基本になります。
これに継承を組み合わせることで、動的にオブジェクトを生成することができるようになります。
で、この FactoryMethod
を使って、いろんなスライムを作るというのが今回のお話です。
ちなみに、抽象基本クラスを分かっておく必要があるので、分からない方は、調べるか、以下を参考にしてみるといいでしょう。
pickles-ochazuke.hatenablog.com
全体イメージ
UML 図を初めて書いたので、間違っているところがあるかも……。
まずは大きく4つに分けて、ISlime
, Slime
, SheSlime
, HealSlime
, Status
。一つずつ簡単に説明します。
Status
これは、スライムのステータスを表します。日本の(RPG)ゲームでいうステータスと英語の Status は、意味が異なるようなのだけど、
ここでは深く考えないようにします。中身は、見てみると何となく分かると思うのだけど、名前(name_
)、HP(hp_
)、MP(mp_
)、攻撃力(attack_
)、防御力(defense_
)、経験値(exp_
)、ゴールド(gold_
)が入っている。モンスター(今回は、スライム属のみ)共通のステータスとなります。
ISlime
スライム属の抽象基本クラスです。ISlime
の I
は、Interface
の I
。
スライム属に属するクラスは、これを継承します。これによってあとで作る FactoryMethod
は、一つの関数で動的にオブジェクトを作れるようになります。name()
, hp()
, mp()
, attack()
, defense()
, exp()
, gold()
は、get 関数で特に意味はなくて、status_
にアクセスする手段を用意しているだけ。あと、コンストラクタ、デストラクタは、実際にはあるのだけど、書いてないです。これは、Slime
, SheSlime
, HealSlime
共通。
Talk()
, Skill()
は、それぞれ純粋仮想関数。継承先は、この二つの処理を実装する必要があります。
ViewInformation()
は、オブジェクトのステータスを出力します。
Slime
スライムを表すクラス。Talk()
, Skill()
をオーバライドしています(あとデストラクタも)。ここら辺は、実行したときにちゃんと Slime のクラスが作られているか分かるようにしているだけなので、実装内容は何でもいいです。
SheSlime
ベススライムを表すクラス。外国だと SheSlime と言うみたいです。内部の説明は、Slime と同じ。もちろん実装内容は、区別できるようにしてあります。
HealSlime
ホイミスライムを表すクラス。これも Slime と同じ。
以上のスライムクラスを使って、FactoryMethod
を作ります。
FactoryMethod
まずは、クラス図を
FactroyMethod を実装するのは、SlimeFactory
というクラス。このクラスは、今回、コンストラクタもデストラクタも必要ないので、delete
しています。DoMakeSlime()
という関数が FactoryMethod
になります。
実際は、FactoryMethod を実装しているクラス内でオブジェクトの管理やシングルトンパターンを実装するようだけど、簡単にするためにそれらは実装していません。
というか生成と管理を一つのクラスで任せるものなのかどうか分からないです……。別々にしても良さそうだけど、どうなんだろう?
クラス図の"関連"を表す矢印の使い方が正しいのか自信はないのだけど、これらをインクルードする必要があるので、"関連"としています。
ただ、ヘッダ(SlimeFactory.h)でインクルードするのは、ISlime のみで、他はソースファイル(SlimeFactroy.cpp)でインクルードしています。
実装
実装内容は以下のようになっています。まずは、ヘッダファイル。
#pragma once #include <string> #include "ISlime.h" class SlimeFactory { public: SlimeFactory() = delete; virtual ~SlimeFactory() = delete; static ISlime* DoMakeSlime(const std::string& type); };
スライム属のクラスの内、インクルードしているのは、 “ISlime.h” のみで、他のクラスはここで知る必要はないです。
コンストラクタとデストラクタは必要なかったので、delete しています。デストラクタを仮想にしているけど必要ないです……。
DoMakeSlime(const std::string& type)
が先に書いたように FactroyMethod になります。type
の値を見て、どのオブジェクトを作るかを決めています。作りたいオブジェクトが分かるなら文字列
でも enum 型
でもただの値
でもいいです。たぶん enum 型
が一番扱いやすいです。
次にソースファイル。
#include "SlimeFactory.h" #include "Slime.h" #include "SheSlime.h" #include "HealSlime.h" ISlime* SlimeFactory::DoMakeSlime(const std::string& type) { if (type == "Slime") { return new Slime(); } else if (type == "SheSlime") { return new SheSlime(); } else if (type == "HealSlime") { return new HealSlime(); } return nullptr; }
こちらでは、実際にオブジェクトを作るために型を知る必要があるので、Slime.h
, SheSlime.h
, HealSlime.h
をインクルードしています。
DoMakeSlime
の処理は、とても簡単で type
の中身を見て、条件式で判定して、その中のオブジェクト生成処理を実行し、そのオブジェクトを返すだけです。型の異なるオブジェクトを返せるのは、継承元が全て同じ ISlime
だから。
戻り値の型を void*
にすれば、継承元が異なる型を返すようにすることも出来るけど、危険だろうし、扱いづらい。それなら抽象基本クラスごとに関数を分けた方がいいと思います。
次は、実際に使うときのコードとその結果を載せます。
実行
main.cpp 内は、以下のようになっています。
#include <iostream> #include "ISlime.h" #include "SlimeFactory.h" int main(void) { ISlime* slime = SlimeFactory::DoMakeSlime("Slime"); slime->Skill(); delete slime; slime = SlimeFactory::DoMakeSlime("SheSlime"); slime->Skill(); slime->ViewInformation(); delete slime; slime = SlimeFactory::DoMakeSlime("HealSlime"); slime->Skill(); delete slime; slime = SlimeFactory::DoMakeSlime(""); if (!slime) { std::cout << "スライムの生成に失敗した!" << std::endl; std::cout << std::endl; } else { slime->Skill(); delete slime; } return 0; }
実行する側は、FactoryMethod のクラスと、抽象基本クラスだけを知るだけで使うことができます。もちろん、継承先にしかない機能を使う場合は、そのクラスのヘッダファイルをインクルードする必要があります。
実行結果は、以下のようになります。
スラりん が現れた! スラりん のスキル発動! 逃げ出す! しかし、逃げられなかった! へんじがない。ただのスラりんのようだ。 スラみ のスキル発動! メラ! しかし、MP が足りない! class SheSlime の情報を表示します ======== スラみ 8 3 10 12 3 1 =============================== ホイミン のスキル発動! ホイミ! ホイミン に 30 の回復! スライムの生成に失敗した! 続行するには何かキーを押してください . . .
おわり
以上、FactoryMethod の基本を書いてみました。上手くクラスを抽象化できれば、FactoryMethod が有効活用できそうです。FactoryMethod を扱うことよりもクラスの抽象化をどうするかの方が難しいのかもしれないですね。
作成したオブジェクトを FactroyMethod の内部(今回だと SlimeFactory)で持つか、作成を依頼してきた側(main関数)に任せるかは、その時々で変わりそう。
最後に、今回のコードを以下に上げておきます。
参考にした資料は以下です。
- 作者: マーティン・レディ,Martin Reddy,三宅陽一郎,ホジソンますみ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2012/10/31
- メディア: 単行本
- 購入: 4人 クリック: 106回
- この商品を含むブログ (11件) を見る
- 作者: エリックガンマ,ラルフジョンソン,リチャードヘルム,ジョンブリシディース,Erich Gamma,Ralph Johnson,Richard Helm,John Vlissides,本位田真一,吉田和樹
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 1999/10
- メディア: 単行本
- 購入: 21人 クリック: 711回
- この商品を含むブログ (210件) を見る
Visual Studio で 動的ライブラリ(DLL)を利用する
前回は、作成するところまで行いました。
今回は、実際に使ってみたいと思います。
pickles-ochazuke.hatenablog.com
DLL を使うプロジェクトを作成
とくに変わったところはないです。いつも通り作っちゃってください。自分は、空のプロジェクトで作成しました。プロジェクト名は、Project3
です。ついでに main 関数を書くためにソースファイル(main.cpp)を作っておきましょう。
DLL を使うための準備
さて、まずは DLL を使えるようにしないといけません。ざっくり手順を書きますと
- ソリューションに DLL のプロジェクトを追加(プロジェクトは、前回作成済み)
- 使う側に DLL の参照を追加する
- DLL のヘッダファイルがあるディレクトリを追加する
1. ソリューションに DLL のプロジェクトを追加
ソリューションエクスプローラ
に表示されているソリューションを右クリックして、追加
> 既存のプロジェクト
を選択します。
するとエクスプローラが開くので、追加したい DLL のプロジェクトを選びます。ここでは、前回作成したプロジェクト(DLLTest.vcxproj
)を選びました。これでソリューションエクスプローラ
に、先ほど選んだプロジェクトが表示されていると思います。これで追加完了です。
2. 使う側にDLL の参照を追加する
次に、今回作成したプロジェクト(Project3)に参照を追加します。画像で見ていただくと分かりやすいと思いますが、ソリューションエクスプローラ
に表示されている、Project3
の下に参照
があると思います。ここを右クリックして、参照の追加
を選びます。
で、前回作成したプロジェクト名が表示されていると思うので、それにチェックを入れます。
3. DLL のヘッダファイルがあるディレクトリを追加する
Project3
を右クリックして、一番下にあるプロパティ
を選択します。で、構成プロパティ
> C/C++
と選び、追加のインクルードディレクトリ
に追加したい DLL のヘッダファイルがあるディレクトリを選びます。前回作成した DLL の場合、DLLTest.h
があるディレクトリを選びます。これで準備完了です。
DLL を使う
実際に使ってみます。手順 1 ですでにソースファイル(main.cpp)を作ってあると思うので、そこに書いていきましょう。
以下のように書きました。
#include <iostream> #include "../DLLTest/DLLTest.h" int main() { DLLTest::HelloWorld hello; DLLTest::HelloWorld::hello(); hello.world(); std::cout << hello.huga << std::endl; return 0; }
DLLTest.h
のインクルード場所は、環境によって異なると思いますので、注意してください。ビルドをして通ったら、実行してみましょう。あ、実行の前に、メニュー
> プロジェクト
> "プロジェクト名"のプロパティ
を選び、構成プロパティ
> リンカー
> システム
を選びます。で、サブシステム
という欄があるので、コンソール
を選び、OK を押します。
実行すると同じであれば、以下のように表示されると思います。
おわり
以上が DLL の使い方になります。プロジェクトとヘッダファイルが必要になるので、どこか共通の場所を作っておくと良いかもしれません。どう管理するといいんだろう……。
以下を参考にしました。
Visual Studio で 動的ライブラリ(DLL)を作成する
自分へのメモ書き(上の画像に意味はないです)。
ライブラリを自分で作ったことがなかったので、ちょいと作ってみることに。
Visual Studio は 2017 です。あと C++ で書きます。
DLL プロジェクトの作成
まず、いつも通り Visual Studio を起動して、新しいプロジェクト
を作成します。
選ぶプロジェクトの種類ですが、Win32 コンソールアプリケーション
を選びます。
で、名前はてきとうで構いません。
簡単で打ち間違いにくい名前にするといいかもしれません。ここでは、DLLTest
にしました。
OK > 次へ > と進み、アプリケーションの種類
では、DLL
を選びます。
他は、何もいじらなくてOKです。完了を押します。
するとプロジェクトが作成され、ソリューションエクスプローラ
のソースファイル
の下には、dllmain.cpp
, DLLTest.cpp
, stdafx.cpp
があり、ヘッダーファイル
には、stdafx.h
, targetver.h
があります。
それぞれ開くと、中にコメントが書かれているので、読んでおくといいかもしれません。まぁ触るのは、DLLTest.cpp
だけなのですが。
DLL の実装
では、ライブラリを作っていきます。まず、ヘッダーファイル
にファイルを追加します。ここは、いつも通り新しいクラスを追加するときと同じように作れば良いです。ファイル名も同じように自由です。ここでは、DLLTest.h
にしました。
そして、先頭に以下を書きます。
#ifdef DLLTEST_EXPORTS #define DLLTEST_API __declspec(dllexport) #else #define DLLTEST_API __declspec(dllimport) #endif
ここで注意して欲しいのは、ifdef DLLTEST_EXPORTS
です。これは、プロジェクト名によって異なりますので注意です。新しいプロジェクトを DLL として作成したときに、Visual Studio 側が勝手に定義してくれます。その名前は、プロジェクト名(全て大文字)_EXPORTS
となります。今回、プロジェクト名は、DLLTest
だったので、DLLTEST_EXPORTS
となるわけです。
あとは、この下にいつものようにクラスを書いていきます。今回は、以下のように書いてみました。
namespace DLLTest { class HelloWorld { public: int huga; DLLTEST_API HelloWorld(); static DLLTEST_API void hello(); DLLTEST_API void world(); private: int hoge = 0; }; }
各関数の手前に、DLLTEST_API
があります。先頭行で書いた define
です。DLLTEST_EXPORTS
が定義されているので中身は、__declspec(dllexport)
となるはずです。__declspec(dllexport)
と __declspec(dllimport)
の違いは、ちゃんと調べていないですが、DLL を作成するときは、dllexport
で、 DLL を利用するときは、dllimport
だと判断しています。
変数には付けなくても問題ないようです(ここら辺のことは、どこを見たら分かるんですかね……)。
次に、DLLTest.cpp
に書いていきます。全文を載せています。
#include "stdafx.h" #include "DLLTest.h" #include "iostream" namespace DLLTest { HelloWorld::HelloWorld() : hoge(32) { std::cout << "HelloWorld " << hoge << std::endl; } void HelloWorld::HelloWorld::hello() { std::cout << "Hello"; return; } void HelloWorld::HelloWorld::world() { std::cout << "World" << std::endl; return; } }
こちらは、とくに変わらずいつも通りに書いてしまえば問題ありません。気をつけるのは、stdafx.h
は一番最初にインクルードする必要があることです。原因はよく分かっていないですが、他のインクルードの後だと、ビルドが通りませんでした。
ビルドをして成功したら、DLLTest のフォルダ下にある Debug フォルダに DLLTest.dll があると思います。
おわり
以上で DLL の作成は完了です。まだまだ何となくで作っていますが、少しずつ理解を深めていこうと思います。そのときに分かったことがあれば、また記事にしたいですね。何か参考になりそうな資料を教えていただければ幸いです。
次回は、作成した DLL を使ってみたいと思います。
pickles-ochazuke.hatenablog.com
以下を参考にしました。
https://msdn.microsoft.com/ja-jp/library/ms235636.aspx
https://msdn.microsoft.com/ja-jp/library/3y1sfaz2.aspx