お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

簡単に 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つの方法があります。

  • コピーコンストラクタ、代入演算子delete(暗黙定義の禁止) する
  • コピーコンストラクタ、代入演算子を明示的に定義する
  • スマートポインタを利用する

個人的には、スマートポインタが楽だと思います。コピーを禁止したい場合は、std::unique_ptr, コピーを許可するなら std::shared_ptr でいいと思います。 スマートポインタを使えば、コピーセマンティクスを書く必要もなくなります。

おわり

Pimpl は、有効に使えると強力な反面、注意する点が多い方法だと感じました。デメリットは小さいので、積極的に使っていきたいですが、プライベートをなんでもかんでも Pimpl に実装するのも違うと思うので、よく考えて使いたいと思います。

Pimpl の書き方をメインに書いたので、どう活用するかは分かりにくかったかもしれません。そういった内容は、参考にした本に載っているのでそちらを参照するといいでしょう。

今回のコードは、以下に上げています。

github.com

参考にした資料です。

C++のためのAPIデザイン

C++のためのAPIデザイン

拡張式 Factory Method

f:id:pickles-ochazuke:20170715173147j:plain

ナッツ食べてます。そこそこおいしいです。

前回、FactoryMethod を作りましたが、今回は、それを改造します。

pickles-ochazuke.hatenablog.com

基本的な FactoryMethod だと、新しいクラスを作ったときに毎度 FactoryMethod 側も新しいクラス用の処理を追加しないといけません。そこで、新しいクラスへの対応を実行中に行えるようにします。

概要

大まかに書くと、抽象基本クラスを継承した新しいクラス側に、"自身のオブジェクトを作成し、それを返す" コールバック関数を作ります。そのコールバック関数と key となる文字列を FactoryMethod に渡し、FactoryMethod 側は、その関係を覚えておきます。実際にオブジェクトが必要になったときは、key となる文字列を渡すことで、それに対応したコールバック関数が FactoryMethod 内で呼ばれ、作成されたオブジェクトが返ってくる。という感じです。

図にすると以下のような感じでしょうか。

f:id:pickles-ochazuke:20170715194958p:plain

上記は、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::stringkey となり、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 を渡されたときに、それに関係する関数ポインタを返せるならどう作ってもいいわけです。なので、今回一番重要なのは、関数ポインタを理解しているかどうかなのだと思いました。

最後に、コードと参考資料を載せておきます。

github.com

C++のためのAPIデザイン

C++のためのAPIデザイン

Factory Method を使って、スライム属を作る

Factroy Methodドラクエのスライムで表現してみる記事。

Factory Method とは

オブジェクト指向における再利用のためのデザインパターンの一つです。そのデザインパターンの中でも生成に関するパターンの一つ。

このパターンによって、オブジェクト型を指定せずにオブジェクトを生成できるようになります。
簡単に説明すると、関数内でコンストラクタを呼び、生成したオブジェクトを返すのがこのパターンの基本になります。 これに継承を組み合わせることで、動的にオブジェクトを生成することができるようになります。

で、この FactoryMethod を使って、いろんなスライムを作るというのが今回のお話です。 ちなみに、抽象基本クラスを分かっておく必要があるので、分からない方は、調べるか、以下を参考にしてみるといいでしょう。

pickles-ochazuke.hatenablog.com

全体イメージ

f:id:pickles-ochazuke:20170714010327p:plain

UML 図を初めて書いたので、間違っているところがあるかも……。

まずは大きく4つに分けて、ISlime, Slime, SheSlime, HealSlime, Status。一つずつ簡単に説明します。

Status

これは、スライムのステータスを表します。日本の(RPG)ゲームでいうステータスと英語の Status は、意味が異なるようなのだけど、 ここでは深く考えないようにします。中身は、見てみると何となく分かると思うのだけど、名前(name_)、HP(hp_)、MP(mp_)、攻撃力(attack_)、防御力(defense_)、経験値exp_)、ゴールド(gold_)が入っている。モンスター(今回は、スライム属のみ)共通のステータスとなります。

ISlime

スライム属の抽象基本クラスです。ISlimeI は、InterfaceI
スライム属に属するクラスは、これを継承します。これによってあとで作る 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

まずは、クラス図を

f:id:pickles-ochazuke:20170714013033p:plain

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関数)に任せるかは、その時々で変わりそう。

最後に、今回のコードを以下に上げておきます。

github.com

参考にした資料は以下です。

C++のためのAPIデザイン

C++のためのAPIデザイン

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

Visual Studio で 動的ライブラリ(DLL)を利用する

f:id:pickles-ochazuke:20170705153431j:plain

前回は、作成するところまで行いました。
今回は、実際に使ってみたいと思います。

pickles-ochazuke.hatenablog.com

DLL を使うプロジェクトを作成

とくに変わったところはないです。いつも通り作っちゃってください。自分は、空のプロジェクトで作成しました。プロジェクト名は、Project3です。ついでに main 関数を書くためにソースファイル(main.cpp)を作っておきましょう。

DLL を使うための準備

さて、まずは DLL を使えるようにしないといけません。ざっくり手順を書きますと

  1. ソリューションに DLL のプロジェクトを追加(プロジェクトは、前回作成済み)
  2. 使う側に DLL の参照を追加する
  3. DLL のヘッダファイルがあるディレクトリを追加する

1. ソリューションに DLL のプロジェクトを追加

ソリューションエクスプローラに表示されているソリューションを右クリックして、追加 > 既存のプロジェクト を選択します。

f:id:pickles-ochazuke:20170705164854p:plain

するとエクスプローラが開くので、追加したい DLL のプロジェクトを選びます。ここでは、前回作成したプロジェクト(DLLTest.vcxproj)を選びました。これでソリューションエクスプローラに、先ほど選んだプロジェクトが表示されていると思います。これで追加完了です。

f:id:pickles-ochazuke:20170705165339p:plain

2. 使う側にDLL の参照を追加する

次に、今回作成したプロジェクト(Project3)に参照を追加します。画像で見ていただくと分かりやすいと思いますが、ソリューションエクスプローラに表示されている、Project3 の下に参照があると思います。ここを右クリックして、参照の追加を選びます。 f:id:pickles-ochazuke:20170705165916p:plain

で、前回作成したプロジェクト名が表示されていると思うので、それにチェックを入れます。

f:id:pickles-ochazuke:20170705170036p:plain

3. DLL のヘッダファイルがあるディレクトリを追加する

Project3 を右クリックして、一番下にあるプロパティを選択します。で、構成プロパティ > C/C++ と選び、追加のインクルードディレクトリに追加したい DLL のヘッダファイルがあるディレクトリを選びます。前回作成した DLL の場合、DLLTest.h があるディレクトリを選びます。これで準備完了です。

f:id:pickles-ochazuke:20170705170257p:plain

f:id:pickles-ochazuke:20170705170143p:plain

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 を押します。

f:id:pickles-ochazuke:20170705170722p:plain

実行すると同じであれば、以下のように表示されると思います。

f:id:pickles-ochazuke:20170705171212p:plain

おわり

以上が DLL の使い方になります。プロジェクトとヘッダファイルが必要になるので、どこか共通の場所を作っておくと良いかもしれません。どう管理するといいんだろう……。

以下を参考にしました。

https://msdn.microsoft.com/ja-jp/library/ms235636.aspx

Visual Studio で 動的ライブラリ(DLL)を作成する

f:id:pickles-ochazuke:20170705153246j:plain

自分へのメモ書き(上の画像に意味はないです)。

ライブラリを自分で作ったことがなかったので、ちょいと作ってみることに。
Visual Studio は 2017 です。あと C++ で書きます。

DLL プロジェクトの作成

まず、いつも通り Visual Studio を起動して、新しいプロジェクトを作成します。
選ぶプロジェクトの種類ですが、Win32 コンソールアプリケーション を選びます。
で、名前はてきとうで構いません。
簡単で打ち間違いにくい名前にするといいかもしれません。ここでは、DLLTest にしました。

f:id:pickles-ochazuke:20170704231404p:plain

OK > 次へ > と進み、アプリケーションの種類では、DLL を選びます。
他は、何もいじらなくてOKです。完了を押します。

f:id:pickles-ochazuke:20170704231409p:plain

するとプロジェクトが作成され、ソリューションエクスプローラソースファイルの下には、dllmain.cpp, DLLTest.cpp, stdafx.cpp があり、ヘッダーファイルには、stdafx.h, targetver.h があります。

f:id:pickles-ochazuke:20170704231751p:plain

それぞれ開くと、中にコメントが書かれているので、読んでおくといいかもしれません。まぁ触るのは、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