ホーム > 使った >

C++Builder Tips

文書

履歴

editor唯野
2001.2.27公開
2007.1.30修正
2012.1.5追加
2012.1.6追加、修正
2012.1.10修正
2012.2.12追加
2012.2.15修正
2012.2.23修正
2014.7.12追加
2014.11.24追加
2015.2.23追加
2018.8.20追加
2018.8.27修正
2018.10.1修正

概要

Tips 集の C++Builder 版です。対象として BCB4/5/6/2007/2009/XE2/XE6 を扱っています。メーカーの宣伝そのままではありますが、C++ という言語のパワーと VCL(Delphi) というライブラリのパワーを両用できる RAD としての生産性の高さでは特筆すべき実力を持った開発ツールです。

# 最近、かなり落ち目だけど...
# 洋書やStackoverflowでも、ろくに情報のない辺り本当に終わってる感が...

C++Builder XE6

LME288 エラーへの対処

Windows10 Pro(x64)で XE6 を動かすとリンク時に上記のエラーが出て PC を再起動しても直らないので調べてみた。基本的に以下の記事にある手順に従えばよい。

cf. https://stackoverflow.com/questions/28929516/c-builder-xe7-lme288-error

まず、http://cc.embarcadero.com/Item/30459 (要EDNログイン)から 30459_lamarker_a_tool_to_set_unset_large_address_aware_bits.ZIP をダウンロードして、その中にある LAMarker.exe を ilink32.exe のあるディレクトリ(XE6 なら C:\Program Files (x86)\Embarcadero\Studio\14.0\bin)へコピーする。

次いで、コマンドプロンプトを管理者として実行し上記ディレクトリへ移動したら ilink32.exeのバックアップを作った上で

lamarker -M -Filink32.exe

を実行する。lamakerの動作についても上記記事に説明がある。

char*とwchar*の相互変換

char*はAnsiString.c_str()、wchar*はUnicodeString.c_str()で得られる。そしてAnsiStringとUnicodeStringは互いにキャスト可能なので、必要とする型にキャストしてからc_str()すればよい。Windows APIに渡す引数などで使う。

AnsiString    command = "perl";
UnicodeString option  = "hoge.pl";
ShellExecute(Handle, NULL, static_cast<UnicodeString>(command).c_str(), option.c_str(), NULL, SW_HIDE);

// キャストを敬遠するのであればサイズ分の器を用意する
int length = command.WideCharBufSize();
wchar_t* cmd = new wchar_t[length];
command.WideChar(cmd, length);
(略)
delete cmd;

midas.dllをexeに含める

ClientDataSetを用いるアプリケーションはmidas.dllを配布する必要があるが、バージョンが合わなかったり、先に登録されているとexeと同じディレクトリにあっても参照されずエラーになる。exeに静的リンクして含めてしまうには以下のようにする。

// メインフォームの.cpp
#pragma comment(lib, "midas.lib")
extern "C" __stdcall DllGetDataSnapClassObject(REFCLSID rclsid, REFIID riid, void **ppv);

// メインフォームのOnCreateイベントハンドラなど
void __fastcall TMainForm::FormCreate(TObject *Sender)
{
    RegisterMidasLib(DllGetDataSnapClassObject);
}

後は[プロジェクト]-[オプション]の[C++リンカ]にある「動的RTLとリンクする」をFalse、同じく[パッケージ]-[実行時パッケージ]の「実行時パッケージを使ってリンク」をFalseにしてビルドする。但し、exeサイズが200KBほど大きくなる。

C++Builder XE2

動的な多次元配列

boost::multi_array が使えないので vector をネストさせる。単純な構造体程度でメンバのメモリ上での配置が自明なら、「sizeof(構造体) x 要素数」でアロケートして、(一次元配列的に)添字 + メンバへのオフセットでアクセスしてもよいと思う。

#include <vector>
using namespace std;

class Hoge
{
private:
    vector< vector< vector<MY_RECT> > > area;

public:
    UINT __fastcall Init(UINT size1, UINT size2, UINT size3)
    {
        area.resize(size1);  // 1段目
        for(UINT i = 0; i < size1; i++)
        {
            area[i].resize(size2);  // 2段目
            for(UINT j = 0; j < size2; j++)
            {
                area[i][j].resize(size3);  // 3段目
            }
        }

        return 0;
    };

    MY_RECT __fastcall Get(UINT i, UINT j, UINT k)
    {
        return(area[i][j][k]);
    };

    int __fastcall Set(UINT i, UINT j, UINT k, MY_RECT x)
    {
        area[i][j][k] = x;
        return 0;
    };
}

スマートポインタを使う

これまた boost ネタだが、new したデータの解放忘れ(メモリリーク)を防ぎたい場合にはスマートポインタが便利。boost::shared_ptr は参照カウントを用いた自動 delete を行ってくれる。継承されたクラスオブジェクトに使用しても(デストラクタが virtual されていれば)正しいデストラクタ呼び出しを行ってくれるが、循環参照にだけは注意が必要。標準の auto_ptr や boost の他のスマートポインタとの違いはググりませう。

#include <boost/shared_ptr.hpp>
using namespace boost;

typedef boost::shared_ptr<HogeBaseClass> HogeShdPtr;

// 以降はオブジェクトの参照カウンタが0になった時点で自動delete
HogeShdPtr hoge(new HogeChildClass());  // ChildをBaseに代入

DataSetのコピー

CloneCursor()を使う

必要なDataSetの範囲に対してコピー前と同じ要領でアクセスできる。参照しているデータの一時的な退避に便利。但し、ユーザが作成した参照項目はコピーされない...

TSimpleDataSet* clone = new TSimpleDataSet(Application);
// dsはコピー元になるDataSet、第2・第3引数でFilterなどを引き継ぐか指定
clone->CloneCursor(ds, False, False);

// 後はクローンを参照する
int c = clone->RecordCount;
int id = clone->FieldByName("id")->AsInteger;

delete clone;
boost::unordered_mapでFieldNameとValueをペアで持つ

というか、これで C++Builder でもハッシュ(連想配列)が使える。ちなみに XE2 には boost 1.3.9 が付いてくるが、multi_array など非対応のものもある。vector に入れてハッシュの配列(擬似的に複数レコードの保持)もできる。

#include <boost/unordered_map.hpp>
using namespace boost;
using namespace std;

boost::unordered_map<string, Variant> hash;

for(int i = 0; i < ds->FieldCount; i++)
{
    hash[AnsiString(ds->Fields->Fields[i]->FieldName).c_str()] =
        ds->Fields->Fields[i]->Value;
}

// 後はハッシュを参照する、配列演算子が文字列を受け付ける
AnsiString hoge = hash["hoge"];
Variant配列に値をコピー

VarArrayCreate() で Variant の配列へコピーするのもお手軽である。いずれにせよ DataSet の各 Field 値は Variant なので Variant とは相性が良い。

Variant array = VarArrayCreate(OPENARRAY(int, (0, ds->FieldCount)), varVariant);

for(int i = 0; i < ds->FieldCount; i++)
{
    array.PutElement(ds->Fields->Fields[i]->Value, i);
}

// 後はVariant配列を参照する
// この場合はそのままループさせて元のDataSetに再代入が可能
for(int i = 0; i < ds->FieldCount; i++)
{
    ds->Fields->Fields[i]->Value = array.GetElement(i);
}

TSimpleDataSetは参照用に使う

TSimpleDataSet は TClientDataSet と TDataSetProvider の合成コンポーネントのようなもので DataSet の参照と更新をこれひとつで行える便利なコンポーネントである。しかし、TSimpleDataSet には TDataSetProvider にある UpdateMode プロパティがなく、更新時には upWhereAll でしか対象レコードの一致を見ようとしないため、これに失敗すると「EDatabaseError : レコードが見つからないか、またはほかのユーザーによって変更されました」となってしまう。

そのため upWhereKeyOnly などで更新レコードを特定したい場合は、TSimpleDataSet -> TDataSource という流れではなく、TSQLDataSet -> TDataSetProvider -> TClientDataSet -> TDataSource というコンポーネントの関連付けにしなければならない。つまり、単純な DataSet でない限り、TSimpleDataSet は参照用として割り切った方がよい。

# 対処方法をご存じの方がおられれば教えてください。

Excel 出力では WideString を使う

BCB 2009 では発生しなかったのだが、XE2 SP3 では Office XP の Excel 2002 に OLE 経由で書き出しをしようとすると「EOleSysError : 変数の種類が間違っています」となり、従来のコードではエラーになる。具体的にいうと OleFunction() や OlePropertySet() などに渡すパラメータ文字列を WideString にする必要がある。私はマクロにして逃げた。

// ここに出てくる filename、str、s は AnsiString である
// BCB 2009
workbook = workbooks.OleFunction("Open", filename.c_str());
とか
cell.OlePropertySet("Value", str.c_str());

// BCB XE2
#define WSP(s) &(WideString(s))

workbook = workbooks.OleFunction("Open", WSP(filename));
とか
cell.OlePropertySet("Value", WSP(str));

C++Builder 2009

Boost::Regex による正規表現を使う

BCB 2009 には Boost 1.3.5 が含まれている。以前から Perl ばりの正規表現は C++ でも使いたい機能の筆頭だったので使ってみた。ちなみにこれらは 2 バイト文字の 1 バイト目でもヒットしてしまうが、BCB/正規表現を使う によれば、2 バイト文字列の場合は型を wregex や wmatch にすればよいらしい(私自身は未検証)。

boostはかなり強力なのだが、個人的に不便だと思うのは文字列型が std::string を想定しており、C++Builder 標準の AnsiString とは直接の互換性がない点である。

//---------------------------------------------------------------------------
// 正規表現検索
// Boost::Regexによる検索、std::string型へ変換してregex_search()を呼び出す
// 正規表現は完全一致でなければならない
//---------------------------------------------------------------------------
bool __fastcall TCommonDataModule::RegexSearch(AnsiString str, AnsiString re)
{
    string std_str = str.c_str();
    regex std_re(re.c_str());
    match_flag_type f = regex_constants::match_any;

    bool result = regex_search(std_str, std_re, f);

    return result;
}

//---------------------------------------------------------------------------
// 正規表現検索(マッチ文字列返却)
// Boost::Regexによる検索、std::string型へ変換してregex_search()を呼び出す
// 正規表現は完全一致でなければならない
//---------------------------------------------------------------------------
bool __fastcall TCommonDataModule::RegexSearch(AnsiString str, AnsiString re, AnsiString* match)
{
    string std_str = str.c_str();
    regex std_re(re.c_str());
    SSIT start = std_str.begin();
    SSIT end   = std_str.end();
    smatch boost_match;
    match_flag_type f = regex_constants::match_default;

    if(regex_match(start, end, boost_match, std_re, f))
    {
        *match = (boost_match.str(1)).c_str();
        return true;
    }

    return false;
}

//---------------------------------------------------------------------------
// 正規表現置換
// Boost::Regexによる置換、std::string型へ変換してregex_replace()を呼び出す
// 正規表現は完全一致でなければならない
//---------------------------------------------------------------------------
unsigned int __fastcall TCommonDataModule::RegexReplace(AnsiString* str, AnsiString re, AnsiString replace)
{
    string std_str = str->c_str();
    regex std_re(re.c_str());
    match_flag_type f = regex_constants::match_any;

    string result = regex_replace(std_str, std_re, replace.c_str(), f);

    *str = result.c_str();

    return 0;
}

C++Builder6

VCL のルーチンを使う

日付文字列の取得やアプリケーションのパスの取得といった処理は、もちろん個別に実装も可能だが、VCL には各種のルーチンとして最初から用意されている。「こんな関数があればいいのに」と思いつくものの多くは既にライブラリからも提供されている。VCL のルーチンとして具体的にどんなものがあるかを知りたければ、ヘルプの [目次]-[VCLリファレンス] にある [ルーチン一覧(カテゴリ別)] などを見ればよい。一度、目を通すだけの時間は明らかに節約できるはずである。

ライブラリを exe に含める

exe サイズとのトレードオフになるが、ソフトウェアの配布においてライブラリを個別に必要としたくない場合は [プロジェクト]-[オプション] で以下のチェックを外してビルドすればよい。

  • [リンカ] タブの「共有 RTL DLL を使う」
  • [パッケージ] タブの 「実行時パッケージを使って構築」

AnsiString.c_str() の結果について

AnsiString 型はメンバ関数である c_str() を用いることにより char* 型(C 形式)の文字列を得ることができる。しかし、この c_str( ) によって得られた文字列は一時オブジェクトであるため、その結果は c_str( ) を使った文の終わり(セミコロン)までしか有効にならない。それゆえ、c_str( ) の結果を char* ポインタに代入しただけでは、次の文ヘ移った時点で既に一時オブジェクトは寿命を終え開放されているため、ポインタの指す先は無効ということになる。仮に中身を読み出すことができたとしても、それはたまたま領域が書き換えられていないというだけのことに過ぎない。

AnsiString str = "hello";

char* ptr = str.c_str();
cout << ptr << endl;  // 既に ptr の指す先は無効

char buf[10];
memset(buf, 0x00, 10);
strncpy(buf, str.c_str(), str.Length() + 1);
cout << buf << endl;  // OK

非ビジュアルコンポーネント名の表示

設計時にフォームへ貼り付けた非ビジュアルコンポーネントの名前は [ツール]-[環境オプション] の [設定] タブにある [コンポーネントのキャプションを表示] で表示することができるようになる。これによって同じ種類の非ビジュアルコンポーネントが複数あるときでも区別がしやすくなる。

便利なショートカット

C++Builder での有用なキーボード・ショートカットを以下に示す。もちろんカスタマイズも含めれば、これ以外にもたくさんあると思う。

コードのテンプレートの挿入 Ctrl+J<br />
有効な変数や引数のリスト表示 Ctrl+Space

ちなみに Shift を押しながら複数のコンポーネントを選択すると、それに共通したプロパティだけがオブジェクトインスペクタに表示される。また、Panel などを Form 全体に貼り付けた場合は Ctrl を押しながら Form をクリックすることで Form 自身を選択できる。

変数やメソッドの命名規則

かなり人によってばらつきがあるものだし、もちろん「これが正しい」というものもないのだが、一応 Borland 的な命名規則を以下に示す。

gグローバル変数
mクラスのデータメンバ
theローカル変数
s構造体
Fプロパティの実体として使うデータメンバ

そして、メソッドの名前では意味のある動詞(特に能動的なもの)や返される値を使うようにする。メソッド名なのに名詞的雰囲気が強いときにはプロパティとして実現できないか再検討する。

例外を IDE に捕捉させない

IDE からの実行で例外が発生した際に AP よりも IDE がこれを先にキャッチして不都合のあるときには [ツール]-[デバッガオプション] の [言語固有の例外] タブにある [Delphi 言語の例外で停止] と [C++ 言語の例外で停止] のチェックを外す。

クラスエクスプローラの凡例

同名のヘルプ・キーワードでシンボルの意味が分かる。これは私の頭ではすぐに忘れるため備忘録的に書いている。

C++Builder5

shlobj.h

BCB5 で shlobj.h をインクルードするとシンボルの多重定義でエラーが出る。これは [プロジェクト]-[オプション] の [ディレクトリ/条件] タブにある [条件定義] に NO_WIN32_LEAN_AND_MEAN を追加することで回避できる。

Up