クラスをエクスポートするDLLについて質問させていただきます。
__declspec(dllexport)を使って通常のクラスをエクスポートすると、
メンバ変数も含まれるため、それらの追加や削除があるたびに
EXE側もビルドし直さなければいけないということは理解しています。
また、その問題の解決案として、
純粋仮想関数のみのクラスをエクスポートしておき、
DLL内部でメンバも持たせた派生クラスを作成するという方法を
ネット上でいくつか読みました。
そこで疑問点があるのですが、この方法の場合、
エクスポートクラスに純粋仮想関数の追加があったときも
EXE側のリビルドは必要無いものなのでしょうか?
> そこで疑問点があるのですが、この方法の場合、
> エクスポートクラスに純粋仮想関数の追加があったときも
> EXE側のリビルドは必要無いものなのでしょうか?
否。その場合はExe側にも影響が波及するのでリビルドが必要です。
実装的にvtblが変わるでしょうし、
意味的にも、そもそも純粋仮想関数が増えたってことはつまり、
「公開してるインタフェースが変更された」ってことですよね。
> __declspec(dllexport)を使って通常のクラスをエクスポートすると、
> メンバ変数も含まれるため、それらの追加や削除があるたびに
> EXE側もビルドし直さなければいけないということは理解しています。
コンストラクタをprivateにして T* Create()friend/static関数をexportするか、
変数全体を構造体にしてポインタを持つようにすれば
再ビルド不要と思うんですが。
興味あるので、どなたかよろしく。
> 純粋仮想関数のみのクラスをエクスポートしておき、
実体が無い場合、exportできないと思います。
pImplイディオムを使って、インターフェース側(エクスポートする部分)と
実装側を分けてやればEXEのリビルドは不要になる。
・・・って、これじゃ話が変わってくるかな。^^;
俺が行うとしたら
インターフェース(public純粋仮想関数のみのクラス)を定義している
ヘッダファイルを公開する。
このインターフェースの派生クラスのインスタンス生成し
これをインターフェースへのポインタとして返す関数のエクスポートだな。
deleteはDLL内で生成したメモリなので同DLL内でdeleteが呼ばれるようにする。
インターフェースに関数が増えた場合
最後に追加するだけなら既存部分の順番は
おそらく変わらないだろうけど気になるなあ。
struct hoge { int a,b,c; };みたいなのは
ちゃんとa,b,cの順になることらしいけど。
exeのリビルドが必要と考えるべきだな。
「純粋仮想関数のみのクラスをエクスポートしておき、…」
「エクスポートクラスに純粋仮想関数の追加があったとき」とあるので、
pImplとか関係なく、Exeに公開してるクラスにメンバ関数を追加すれば、
それはまぁリビルドが必要だろうと。
# defで序数固定してvtblの互換性を維持して…とかいう曲芸は
# できうるのかもしれませんが…I/Fを変更したら素直にリビルドするか、
# (COMとかのように)別クラスとして公開するべきかと。
wclrp ( 'o') さんに概ね同意。私も似たようなことをするかCOMにでもします。
> struct hoge { int a,b,c; };みたいなのは
> ちゃんとa,b,cの順になることらしいけど。
この例では全部public:(アクセス指定が変わっていない)なので
「非静的なデータメンバ変数(Nonstatic data members)のアドレス配置順」は
確かに保証されてますけど、言語仕様にはvtblなんて規定はないですし、
勿論メンバ関数の配置には保証がありません…。
> コンストラクタをprivateにして T* Create()friend/static関数をexportするか、
> 変数全体を構造体にしてポインタを持つようにすれば再ビルド不要と思うんですが。
こんなのを定義して、
class __declspec(dllexport) Foo
{
private:
Foo(){}
Foo(const Foo&);
Foo& operator =(const Foo&);
public:
virtual ~Foo(){}
static Foo* Create();
static Destroy(const Foo*); // Dllで確保したメモリをExeで開放すると、
// 実行時環境が異なってたら痛いので…。
};
実装側で、
class FooImpl : public Foo
{};
Foo* Foo::Create() { return new FooImpl(); }
みたいなの(もしくはFooImplをFooから派生せず、ポインタとしてFooに持たせるpImpl)を
イメージしてると思うのですが、この場合、
*Fooは変更せずに* FooImplだけ変更するなら確かにリビルドは不要になります。
ただ、質問者さんが聞いているような、
「エクスポートクラスに純粋仮想関数の追加があったとき」(Fooを変更)だと、
一般論でいえば、リビルドが必要だと思います。
# 相当注意を払えば実装上大丈夫とかはありえるかもしれませんが。
> 実体が無い場合、exportできないと思います。
インスタンス化できない純粋仮想なクラスでも__declspec(dllexport)は可能です。
(C++によるinterface実装の典型的なパターンかと思います)
> __declspec(dllexport)を使って通常のクラスをエクスポートすると、
> メンバ変数も含まれるため、それらの追加や削除があるたびに
> EXE側もビルドし直さなければいけないということは理解しています。
うまくやれば再ビルドが不要になるのでは、と思い
>コンストラクタをprivateにして T* Create()friend/static関数をexportするか
この方法は、EXE側が実体をもてないようにする思いつきです。
class __declspec(dllexport) Foo
{
private:
Foo(){}
Foo(const Foo&);
Foo& operator =(const Foo&);
~Foo(){} // こっちに移動し、virtualをはずしました
public:
static Foo* Create();
void Destroy(); //非スタティックメンバーに変更しました
bool Do();
// bool Do_ex(); //将来追加する
private:
int x;
int y;
// int z; //将来追加する
};
Do_ex()やzを追加した時にEXEの再ビルドは必要ですか。
> 変数全体を構造体にしてポインタを持つようにすれば再ビルド不要と思うんですが。
これはオブジェクトのサイズを変えないようにする思い付きです。
class __declspec(dllexport) Foo
{
public:
Foo() : p(new Data){}
Foo(const Foo&);
Foo& operator =(const Foo&);
~Foo(){ delete p; }
bool Do();
// bool Do_ex(); //将来追加する
private:
Data* p;
};
struct Data
{
int x;
int y;
// int z; //将来追加する
};
このやり方もまずいですか。
> インスタンス化できない純粋仮想なクラスでも__declspec(dllexport)は可能です。
ありがとうございます。調べてみます。
みなさん多くのご意見ありがとうございます。
ロマさんの提案されている方法は自分もぜひご意見いただければと思いますが、
以下の部分について確認させていただきますでしょうか。
関数のみをエクスポートするDLLのときには、
関数が増えたときも、defファイルに追記していくぶんには、
過去にビルドしたEXEは通常そのまま使えますよね。
> Exeに公開してるクラスにメンバ関数を追加すれば、
> それはまぁリビルドが必要だろうと
そうなると、wclrp ( 'o')さんの言われているような
> インターフェース(public純粋仮想関数のみのクラス)を定義している
> ヘッダファイルを公開する。
の方法(自分がネット上で読んだものもこのような方法でした)は、
「private部分に変更があっただけのときはリビルドの必要が無くなる」
というメリットがメインで、
「public純粋仮想関数に追加があったときは、
過去にビルドしたEXEがそれを使わないままでもリビルドは必要になる」
ということになるのでしょうか?
要は、エクスポートされたvtblに変更があれば、それをインポートする側は
再コンパイルが必要だと考えます。
任意のDLLからエクスポートされた任意のクラスのvirtualなメンバ、
コンストラクタ、又はデストラクタが変更された場合、そのクラスの
vtblは変更されている可能性があります。
つまり、あるvirtualメンバ関数に変更があったDLLについて、それを参照している
実行ファイルの再コンパイルが必要ないと確信が持てるのは、
そのメンバ関数が単独でエクスポートされている場合に限られると
考えられます。また、そのメンバ関数は、インライン展開されない
と確実に言える場合のみ、との条件が付くと考えられます。
自分は、後から追加されたvirtualなメンバが、元のvtblに影響の出ない
部分に追加されるとの確信を持てません(vv;)。
>そこで疑問点があるのですが、この方法の場合、
>エクスポートクラスに純粋仮想関数の追加があったときも
>EXE側のリビルドは必要無いものなのでしょうか?
以上の私見から、必要だと考えます。
>「private部分に変更があっただけのときはリビルドの必要が無くなる」
もちろんprivateなメンバからpublicでvirtualなメンバが
呼び出される可能性は排除できないので、public protected private
に関係なく、一般的には参照側の再コンパイルが必要だと考えざるを得ません。
> 「public純粋仮想関数に追加があったときは、
> 過去にビルドしたEXEがそれを使わないままでもリビルドは必要になる」
一般論でいれば、そうなると思います。
どうしてもリビルドしたくないなら、*以前のクラスはそのままに*
変更した別のクラスも別途提供する、という方が一般論では、安全かと。
# COMの仕組みを調べてみると参考になるのではないでしょうか。
他の皆さんが言われているように基本的にクラスでインターフェイスを
とるDLLで実行ファイル側のリビルドが不要なケースは極限られたケースで
基本的にはリビルドが必要と考えるべきだと思います。
実行ファイルのリビルド不要に重きを置きすぎると返って良くないように感じます。
仲澤@失業者さん、Banさん、PATIOさん、
引き続きご意見ありがとうございます。
> 他の皆さんが言われているように基本的にクラスでインターフェイスを
> とるDLLで実行ファイル側のリビルドが不要なケースは極限られたケースで
> 基本的にはリビルドが必要と考えるべきだと思います。
ネットなどでよく見かける、
純粋仮想クラスをエクスポートするDLLを作る方法のメリットは、
「極限られたケース」のみということになるのですね。
となると、公開するメンバ関数を追加したりしない限りは、
private部分に変更があることもあまり無いでしょうし、
通常のクラスのままDLLにしてしまっても、
リビルドの機会は大して変わらないような気がします。
済マークは付けさせていただきますが、勘違いしている部分があれば、
引き続きアドバイスいただけると嬉しいです。
クラスをエクスポートという時点でEXEをVCで作る縛りになる。ネットでよく見かけるとしても、限られた話でのことになるね。
多分、あなたのいうエクスポートと俺のいうエクスポートは違う意味で使っているのでしょうけど。
インターフェースが変わらないならリビルド不要
そうじゃないとこの世にある大量のプラグインを頻繁にバージョンアップしないといけなくなる。