雑な k|m の生態について

その 42 - 空の HDD に何を詰めよう?

2003-09-18

[C/C++] COM が持っているモノ 3 - addRef()

ぶっちゃけ,こんなコードはどうよって事です:

dll.hpp (本体,DLL 共通)
class ISomeClass {
public:
  virtual void addRef() = 0;
  virtual void release() = 0;
  virtual void *queryInterface(int IID) const = 0;

  virtual void method() = 0;
};

class ISomeClass2 : public ISomeClass {
public:
  virtual void method2() = 0;
};

#define ID_ISomeClass   0
#define ID_ISomeClass2  1

DLL_API_(ISomeClass *) createSomeClass();
dll.cpp (DLL 側)
class SomeClass : public ISomeClass2 {
  int refcount_;
public:
  SomeClass() { refcount_ = 1; }

  void addRef() { refcount_++; }

  void release() { if (--refcount_ == 0) delete this; }

  void *queryInterface(int IID) const {
    if (IID == ID_ISomeClass) {
      addRef();
      return this;
    }
    if (IID == ID_ISomeClass2) {
      addRef();
      return this;
    }
    return 0;
  }

  void method(){}
  void method2(){}
};

ISomeClass * __stdcall createSomeClass() {
  return new SomeClass();
}

要するに参照カウンタの導入です。特に queryInterface() の中でも参照カウンタを増やしている事に注目。これで下のような main() を書ける事になりました。

main.cpp (本体側)
#include "dll.hpp"

int main(int ac, char *av[]) {
  ISomeClass *sc = createSomeClass();
  ISomeClass2 *sc2 = (ISomeClass2*)sc->queryInterface(ID_ISomeClass2);

  sc->release();
  sc2->release();  // これをやってよい (というか,むしろ必要)

  return 0;
}

そこでこんなテンプレートクラスを書いてやると・・・

autoPtr.hpp
template <typename T>
class autoPtr {
  T *ptr_;
public:
  autoPtr( T *ptr ) { ptr_ = ptr; }
  ~autoPtr() { ptr_->release(); }
  T *operator -> () const { return ptr_; }
};

なんと,main() 内で release() を省略できるようになるのです。このクラスは MS-COM の CComPtr<> や XPCOM の nsCOMPtr<> に当たるもの。

main.cpp (本体側)
#include "dll.hpp"
#include "autoPtr.hpp"

int main(int ac, char *av[]) {
  autoPtr<ISomeClass>  sc( createSomeClass() );
  autoPtr<ISomeClass2> sc2( (ISomeClass2*)sc->queryInterface(ID_ISomeClass2) );

  // 演算子 -> でポインタ本体にアクセス
  sc->method();
  sc2->method2();

  // release() は不要!

  return 0;
}

これが COM の入り口だったりするので非常に畏れ。

2003-09-17

[C/C++] COM が持っているモノ 2 - queryInterface()

今日はなんとか createSomeClass()createSomeClass2() を統一させてみたいと思います。作るモノも作り方も同じなのに,欲しいインタフェースが異なるだけで関数を分けなければならないとは,納得がいかないのです。

dll.hpp (本体,DLL 共通)
// 面倒くさいものはいろいろ略
class ISomeClass {
public:
  virtual void *queryInterface(int IID) const = 0;
};

class ISomeClass2 : public ISomeClass {
public:
  (略)
};

#define ID_ISomeClass   0
#define ID_ISomeClass2  1

DLL_API_(ISomeClass *) createSomeClass();
/* もはや createSomeClass2() は必要ない */
dll.cpp (DLL 側)
class SomeClass : public ISomeClass2 {
public:
  void *queryInterface(int IID) const {
    if (IID == ID_ISomeClass) return this;
    if (IID == ID_ISomeClass2) return this;
    return 0;
  }
};

ISomeClass * __stdcall createSomeClass() {
  return new SomeClass();
}
main.cpp (本体側)
#include "dll.hpp"

int main(int ac, char *av[]) {
  ISomeClass *sc = createSomeClass();
  ISomeClass2 *sc2 = (ISomeClass2*)sc->queryInterface(ID_ISomeClass2);

  sc->release();

  // sc2->release();  -- これは不要! 気をつけなければならない?

  return 0;
}

このあたりがよく分からないんだけど,dynamic_cast<>() はコンパイラ依存だという事で,COM のような技術に際しては好まれないようなのです。代わりに同じような機能を実装するのが queryInterface()。動的クラス情報を自前でやりくりするので,いちいちインタフェース毎に ID をつけてやらないといけないのが面倒くさいところ。この面倒くささは MS-COM でも XPCOM でも未解決という事で,どうかひとつ。

しかし注意しなければならないのが,上記コードの scsc2 は同じインスタンスを指しているという事実。片方を release() したならばもう片方は release() してはいけません

普通のポインタでも片方を delete すればもう片方を delete してはいけないというのは当たり前なんだけど,ここで少し工夫をすれば,もっと楽しい事を起こせるのです。

2003-09-16

[C/C++] COM が持っているモノ 1 - release()

COM では仮想デストラクタを持てないため,DLL 側でオブジェクトをデストラクトする,中身が同じような関数を大量に用意しなくてはなりません。こんなのはアホです。イヤです。・・偉い人はここでも考えました。「仮想デストラクタを持てないなら,仮想関数でデストラクトすればよいのでは?」

dll.hpp (本体,DLL 共通)
class ISomeClass {
public:
  virtual void release() = 0; // <- これ!
  (略)
};

class ISomeClass2 : public ISomeClass {
public:
  (略)
};

DLL_API_(ISomeClass *) createSomeClass();
DLL_API_(ISomeClass2 *) createSomeClass2();
dll.cpp (DLL 側)
class SomeClass : public ISomeClass2 {

 (他は略)

  //! インスタンス破棄
  virtual void release() {
    delete this;
  }
};
main.cpp (本体側)
#include "dll.hpp"

int main(int ac, char *av[]) {
  ISomeClass *sc = createSomeClass();
  sc->release();
  return 0;
}

これの何が優れているって,あのうざったい delete***() を書かなくてよいのです。ISomeClass3 を追加したって release() を書き換える必要すらありません。

この調子で createSomeClass()createSomeClass2() も統一できないかなあ?

2003-09-15

[C/C++] COM に毒される前に 4 - 拡張性の問題

って,完全に企画モノのノリだなあ。。。JPEG デコーダの時みたいに,悲惨な事になりませんように・・・。

さて,なんとかしてオブジェクトの破棄方法を考えなければなりません。そういえば ISomeClass がコンストラクタを持てないという理由で createSomeClass() 関数を作ったのだから,deleteSomeClass() 関数とやらも作ってみては?

dll.hpp (DLL,本体共用)
DLL_API_(void) deleteSomeClass( ISomeClass * );
dll.cpp (DLL 側)
void __stdcall deleteSomeClass( ISomeClass *sc ) {
  delete reinterpret_cast<SomeClass *>(sc);
    // きちんと ~SomeClass() が呼ばれるようにキャストせよ!
}
main.cpp (本体側)
#include "dll.hpp"

int main(int ac, char *av[]) {
  ISomeClass *sc = createSomeClass();
  deleteSomeClass( sc );
  return 0;
}

ん,これは良さげ。reinterpret_cast が鼻につきますが,これは仕方ありません。

さてさて,こんな感じのモデルでも,なんと継承,インタフェースの拡張が可能なのです。

dll.hpp 改造
// これらのクラスと関数を追加
class ISomeClass2 : public ISomeClass {
public:
  virtual void method4() = 0;
};

DLL_API_(ISomeClass2*) createSomeClass2();
DLL_API_(void) deleteSomeClass2( ISomeClass2 * );

注意するべきなのは,createSomeClass() はそのまま ISomeClass インタフェースを返すコンストラクタとして残しておかなければならないという事。ヘタに ISomeClass2 を返そうと書き換えると,旧いバージョンの本体 exe にて,新しい DLL ファイルから createSomeClass() エントリポイントが取得できなくなる可能性もあるのです。

このインタフェースの実装には,元々あるクラス SomeClass を使っちゃえばいいと思いませんか? そうしたって,きちんと旧い ISomeClass インタフェースを返す createSomeClass() だって動きます。互換性に問題はありません。

// SomeClass の実装
class SomeClass : public ISomeClass2 {
  int member1_;
public:
  SomeClass() { member1_ = 0; }

 // ISomeClass より
  void method1() { member1_ = 1; }
  void method2() { member1_ = 2; }
  void method3() { member1_ = 3; }

 // ISomeClass2 より
  void method4() { member1_ = 4; }
}

// もともとのコンストラクタ
ISomeClass * __stdcall createSomeClass() {
  return new SomeClass();
};

// 新しいコンストラクタ
ISomeClass2 * __stdcall createSomeClass2() {
  return new SomeClass();
};

// もともとのデストラクタ
void __stdcall deleteSomeClass( ISomeClass *sc ) {
  delete reinterpret_cast<SomeClass *>(sc);
}

// 新しいデストラクタ
void __stdcall deleteSomeClass2( ISomeClass2 *sc ) {
  delete reinterpret_cast<SomeClass *>(sc);
}

・・・おっと・・・createSomeClass()createSomeClass2() の中身,それに deleteSomeClass()deleteSomeClass2() の中身が一緒です。私たちプログラマはこのような事が大嫌いなはずです・・・。

なぜこんな無駄が起きたのかを確認しておくと:それは ISomeClass に仮想デストラクタを持たせられないからですね。

本日のまとめ

まとまりがなくなったので整理。

DLL を越えるクラスのデストラクタには,deleteSomeClass() のような関数を書けばよい事が分かりました。しかし,それはなんだか余計なコードが大量生産されそうな気配です。

そこで,いよいよ COM / XPCOM のアイデアを拝借する事にします。

2003-09-14

[C/C++] COM に毒される前に 3 - "インタフェース" という発想

「クラスのメモリレイアウト」を変更してしまうと,DLL 交換だけではアプリケーションがクラッシュしてしまいます。特に Windows ではこれは致命的なミスで,少しでも運が悪ければ OS を巻き込む事故に発展します。

そこで偉い人は考えました。「メモリレイアウトの変わりようのないもの = インタフェースという概念を導入してみては?」と。

dll.hpp (DLL,本体共通)
// そうだ,こんなマクロを使おう
#ifdef MAKING_DLL
# define DLL_API_(_Ty)  __declspec(dllexport) _Ty __stdcall
#else
# define DLL_API_(_Ty)  __declspec(dllimport) _Ty __stdcall
#endif

// SomeClass インタフェースの宣言
class ISomeClass {
public:
  virtual void method1() = 0;
  virtual void method2() = 0;
  virtual void method3() = 0;
};

// SomeClass を生成する関数 ... ISomeClass インタフェースはコンストラクタを持てないから
DLL_API_(ISomeClass *) createSomeClass();
dll.cpp (DLL 側)
#include "dll.hpp"

// SomeClass の実装
class SomeClass : public ISomeClass {
  int member1_;
public:
  SomeClass() { member1_ = 0; }
  void method1() { member1_ = 1; }
  void method2() { member1_ = 2; }
  void method3() { member1_ = 3; }
};

ISomeClass * __stdcall createSomeClass() {
  return new SomeClass();
}
main.cpp (本体側)
#include "dll.hpp"

int main(int ac, char *av[]) {
  ISomeClass *sc = createSomeClass();
  sc->method1();
  delete sc;  // <- ouch!
  return 0;
}

本体側には ISomeClass しか見えません。ISomeClass 自身を変更しない限り,いくら SomeClass の中身を変更したって痛くも痒くもありません。これで,本体側は一切コンパイルし直さずに,DLL の交換だけで万事解決。

SomeClass クラスは ISomeClass クラスから派生しているんだから,ISomeClass には仮想デストラクタが必要だって? ・・・これは噂に聞いただけで実際に調査してはいないのですが,ISomeClass クラスに仮想デストラクタを持たせると,メモリレイアウトが変わってしまうかもしれないのだそうで。

しかし大丈夫! SomeClass クラスでの実装を単純なものにしておけば ── ハッキリ言ってデストラクタ不要な設計にしておけば ── 仮想デストラクタなぞ不要です!・・・

・・・ダメですね。そもそも DLL 側で new したものを本体側で delete している事自体,そりゃキミやっちゃいかん事です・・・。

2003-09-13

地震の予感

EPIO応援班

未だに「んなもん,分かるわけねーじゃん」とのたまう方もらっしゃいますが,"神の降し給う確率的鉄槌" でもあるまいし,私は地震の予測は可能だと思うんです ── 今現在の予測の方法が正解かどうかは知らないし,今現在の技術で可能かどうかも知らないけど。

ひんまがったプレートかいつ弾けるか? ・・・それは確率的な問題かも分からんね・・・。

[C/C++] COM に毒される前に 2 - これじゃダメな理由

DLL は何といっても「本体プログラムを一切触らずにモジュールを交換する事ができる」点で大変便利。という事で,こういうコードを考えて見ます。

dll.hpp (DLL 側)
class DLL_API C{
  int member1_;
public:
  C();
};
dll.cpp (DLL 側)
C::C() {
  member1_ = 0;
}
main.cpp (本体側)
#include "dll.hpp"

int main() {
  C c;
  return 0;
}

特に問題はないようです。では次,DLL 側のみ次のようにいじります。

dll.hpp (DLL 側)
class DLL_API C{
  int member0_;  // <- これを追加しただけ
  int member1_;
public:
  C();
};

DLL だけを交換して プログラムを走らせると,

いつもの [アプリケーションは終了します] ダイアログ・・

まれに名前だけは聞きますが,これは「クラスのメモリレイアウト」というものを変更したからなんでしょうね。C::member0_ を追加したために,本体モジュールにとって C::C() が存在するはずだった場所に C::member1_ がずれ込んで来てしまったのです。コンストラクタこそ正常に起動されるようですが,この状態でメンバをいじると,えーと,・・・何が起こるか分かりません。

こんなんじゃ DLL としての価値がないので,なんとかしなければ。

[ なんでしょうね ]
ですよね? っていうか自信がないなあ。。。

2003-09-12

[C/C++] COM に毒される前に 1 - クラスを DLL に追いやりたい?

C++ のクラスを DLL に追いやるとしたら,VC++ ではこんなコードを書くそうです。

dll.hpp
#ifndef DLL_HPP_
#define DLL_HPP_

#ifdef MAKING_DLL
# define DLL_API __declspec(dllexport)
#else
# define DLL_API __declspec(dllimport)
#endif

class DLL_API DLLClass {
public:
  DLLClass();
  void func();
};

#endif // DLL_HPP_
dll.cpp
#include "dll.hpp"
#include <stdio.h>

DLLClass::DLLClass() {
  puts("ctor");
}

void DLLClass::func() {
  puts("func");
}

コンパイラの D スイッチで MAKING_DLL というマクロを定義してやって ── VC++ ならば /D "MAKING_DLL" を追加してやって ── .lib ファイルと .dll ファイルが出来上がります。使う時はプロジェクトに .lib ファイルを追加して,こんなコード

#include "dll.hpp"

int main( int ac, char *av[] ) {
  DLLClass d;
  d.func();
  return 0;
}

あら簡単。こうやって作られた DLL はなかなか賢くて,ここで DLLClass を改造して

dll.hpp 改造 1
// !!!!!本当はとっても危険!!!11
class DLL_API DLLClass{
  int member_;
public:
  DLLClass();
  virtual ~DLLClass();
  virtual void extra_func() = 0; // <- what!?
  void func();
};

メンバを増やしたり仮想デストラクタをつけたり,純粋仮想関数までつけてやったりしましたが,DLL の作成は可能です。そして .dll ファイルの交換だけでなぜか無事に動きます

しかし,これはさすがにアウト:

dll.hpp 改造 2
class DLL_API DLLClass{
public:
  DLLClass();
  virtual void func();
};

これで DLLClass::func() の名前解決がうまくいかなくなるようで,アプリケーションの起動段階でエラーを検出し停止します。

・・・もうちょっと遊んで見ます。

2003-09-11

[C/C++] __LINE__ 文字列化

定義済みマクロ __LINE__ は通常は数値リテラルとして展開されるため,

// コンパイルできません
#include <stdio.h>
#define __HERE__  __FILE__ "(" __LINE__ ")"

int main() {
  puts( __HERE__ " : here comes!");
     // "file.c(6) : here comes!" と出力してほしいのに,,,
  return 0;
}

これはコンパイルできません。なんとかしてプリプロセッサのトリックを用いて文字列化する事ができそうな気がしていましたが,それも思いつかずに断念。

ム板@2ch のスレッド "トリッキーなコード その2" でついに発見しました。これは 566 氏によるトリックを,私が欲しかったマクロに仕立て直したもの:

#define TO_STRING( x ) #x
#define _THRU(x) TO_STRING(x)
#define __LINESTR__   _THRU(__LINE__) // これが欲しかったのだよ!

こんなコードを書く事ができます。

#include <stdio.h>

#define TO_STRING( x ) #x
#define _THRU(x) TO_STRING(x)
#define __LINESTR__   _THRU(__LINE__)

#define __HERE__  __FILE__ "(" __LINESTR__ ")"

int main() {
  puts( __HERE__ );
  puts( __HERE__ );
  puts( __HERE__ );
  return 0;
}

それぞれの puts() で,ファイル名と該当する行番号が出力されます。2003-03-05 ではナメたコードを書いていますが,これも __LINESTR__ マクロで解決。

2003-09-10

[C++] アクセス権を変更するには

Mozilla のコードを眺めていて発見しました。継承関係のあるクラス間のメンバのアクセス権を操作するには,これが一番スマートな模様です。

class base {
protected:
  void func() { }
};

class changed : public base {
public:
  using base::func;
};

int main( int ac, char *av[] ) {
  changed c;
  c.func(); // now it's ok.
  return 0;
}

上記の例では,protected である base::func()changed クラスで public にまでひきずり出しています。この using の使い方により,public なメンバを protectedprivate に,protected なメンバを publicprivate にする事が出来ます。

てか「プログラミング言語 C++」にもちゃーんと書いとりますね。大量な情報の中に埋もれていますが。。。

2003-09-09

[C++] VC++5.0 でもできた template 特殊化

実際にいろいろ制約があるので私はどうも VC++5.0 コンパイラを信用していなかったのですが,template 特殊化ならばできたわけですな。そういえば,int 型をテンプレート引数にとる場合の,数値による特殊化が可能な事は 2003-02-17 で確認していました。

#include <stdio.h>

// "万物の起源" クラス
class dcIArche { };

// COM のものまね
template<typename T>
class dcCOMPtr {
public:
  void func(){ puts("typename T"); }
};

// COM のものまね
// 正確には,mozilla の nsCOMPtr<nsISupports> のまね
template<>
class dcCOMPtr<dcIArche> {
public:
  void func(){ puts("typename dcIArche"); }
};

int main( int ac, char *av[] ) {
  dcCOMPtr<int> di;
  dcCOMPtr<dcIArche> da;
  di.func();  // "typename T" を出力
  da.func();  // "typename dcIArche" を出力
  return 0;
}

ならば VC++5.0 では template についてどのような制限があったのかというと・・

template<typename T>
class dcCOMPtr { };

template<typename T>
class dcCOMPtr <T*> { };
    // error C2989: 
    //    このテンプレート クラスはすでに非テンプレート クラスとして定義されています。

特殊化は特殊化でも「部分特殊化」てやつですね。なんだかよく分からないエラーメッセージです。

・・別にいいです。あんまし使わないんで。

[ 万物の起源 ]
アルケー。哲学用語です。

2003-09-08

[C++] CSV を読むエキスパート(?)

->csvread.hpp

で,調子にのってこんなのを作るわけですよ。

#include <iostream>
#include <fstream>
#include <string>

#include "csvread.hpp"

using namespace std;

int main( int ac, char *av[] ) {
  if ( ac < 2 ) return 0;

  ifstream in( av[1], ios_base::binary | ios_base::in );
  csv_reader<char> csv( in );

  string slist[6];

  do {

   // csv から読む
    csv >> slist[0], slist[1], slist[2], slist[3], slist[4], slist[5];

   // 出力
    cout << slist[0] << ','
         << slist[1] << ','
         << slist[2] << ','
         << slist[3] << ','
         << slist[4] << ','
         << slist[5] << endl;

  } while (! csv.eof());

  return 0;
}

operator>>() でストリームから 1 行を読み込み,最初のカラムを読み込みます。続く operator,() でコンマ区切りの値を次々に読み出します。

operator,() も,こんな使い方ならばなんとなく自然に見えます。今日の仕事は◎。

2003-09-07

[C++]「行」を読むエキスパート

->lreader.hpp

std::basic_istream クラスにはきちんと getline() メソッドが定義されているわけですが,しかしこのメソッドは次の点で不満が残ります。

'\r' (Carriage Return) を扱えるの?
もし改行を '\n' と決めうちで getline() メソッドを実装されると,'\r' の存在が非常に厄介なものになります。私は getline() 実装者を信用していない屑人間。
どんな改行文字があったのか?
もし getline() 実装者が '\n''\r' もきちんと扱えるように実装したとしても,std::basic_istream およびそれから派生した STL クラスは,どのような改行文字があったのかを知る手段がありません。

という事で作ってみたのが今日のコード。std::basic_istream クラスから派生した何かをコンストラクタの引数にとり,ストリームから行単位で std::string に読み込みます。認識する改行は '\n'(UNIX スタイル) '\r'(Mac スタイル) '\r\n'(MS-DOS スタイル) '\n\r'(謎のスタイル) それに end-of-stream の 5 つ。

#include <iostream>
#include <fstream>
#include <string>

#include "lreader.hpp"

using namespace std;

int main( int ac, char *av[] ) {

  if ( ac < 2 ) return 0;

  ifstream in( av[1], ios_base::binary | ios_base::in );
  stream_line_reader<char> lreader( in );

  do {
    string str;
    int br = lreader.read_a_line( &str );

    cout << str << (
      (br == 0) ? "[EOF]" :
      (br == 1) ? "[\\n]" :
      (br == 2) ? "[\\r]" :
      (br == 3) ? "[\\r\\n]" : "[\\n\\r]"
    ) << endl;

  } while (! lreader.eof());

  return 0;
}

というコードで

C:\>lineread  something.txt
line 1[\n]
line 2[\n\r]
line 3[\r]
line 4[\r\n]
[EOF]

のような出力が得られます。

ただしこのストリームクラスは協調性のないわがまま坊主で,コンストラクタに渡した basic_istream オブジェクトを独占してしまいます。内部でストリームのキャッシュを行うため,stream_line_reader から 1 行だけ読み込んで,残りのデータを元の basic_istream オブジェクトから読み込む・・なんて動作は,正常には期待できません。

Written by kuri|minima(tkuri@fat.coara.or.jp) - all rights reserved.(warai
このリソースの位置情報は http://www.coara.or.jp/%7etkuri/D/042.htm で安定しています。