RAII:Resource Acquisition Is Initialization
RAII(Resource Acquisition Is Initialization)って、ご存じですか? 直訳すれば「リソースの確保は初期化時に」、実装寄りに意訳(?)すると「必要なリソースはコンストラクタで確保しとけ」ってテクニックですけど、これの裏というか対になるのが「不要になったリソースはデストラクタで解放(返却)しろ」です。
開発屋さんからまたまた小さなツールの作成を頼まれました。「XMLの中から<log time="~" msg="~"/>という<log>要素を抽出しtime,msg属性値をcoutにカンマ区切りで出力してくれ」ってんです。どうやらXML内に点在するデバッグ用のデータをまとめてCSVで出力すればExcelで読めて楽じゃね?って魂胆らしい。
実装にはXMLParserの定番:Apache Xerces-C++をつかいましょうか。今回の処理はXML要素の親子関係は無視し、特定の要素を抽出するだけなのでXMLの構造(階層)まるごとメモリに蓄積するDOMParserはオーバー・スペックと判断し、イベントドリブン型の軽量パーサ:SAXParserを使うことにしました。実装のアウトラインはこんな感じ:
class SAXHandler : public xercesc::HandlerBase {
protected:
virtual void startElement(const XMLCh* const name, xercesc::AttributeList& attributes) {
/* 要素が見つかるたびにココに飛び込む。
nameが"log"であったら
attributesから time,msg属性を取り出し、
カンマ区切りで出力する */
}
};
int main(int argc, char* argv[]) {
...
xercesc::SAXParser parser;
SAXHandler handler;
parser.setDocumentHandler(&handler); // 上記ハンドラをセットして
parser.parse(argv[1]); // パース開始
...
}
ここまでは何ということもありません。キモとなるのはSAXHandler::startElement()のナカミです。SAXParserが読み取ってstartElement()に渡してくれる要素名(name)の型はXMLCh*,属性を読み取るにはconst XMLCh* AttributeList::getValue(const XMLCh*)、なんにせよXMLChを相手にせにゃなりません。ここでXMLChとwchar_tが同じものならためらいなくstd::wstringに仲介してもらえます(Visual C++/Xerces-cでXMLを扱うときはそうしてました)。ところが今回のターゲットはLinux、sizeof(XMLCh)は2であるのに対しsizeof(wchar_t)は4、両者は別物のようで、std::wstring使って楽できそうにはありません。
XercesのAPIを探ってみたところ、2つの変換関数:
- char* xercesc::XMLString::transcode(const XMLCh*)
- XMLCh* xercesc::XMLString::transcode(const char* )
を見つけました。これでなんとかなりそう...ただし、transcode()による変換で得られた文字列はヒープから確保したリソースであり、用が済んだら必ず xercesc::XMLString::release(XMLCh**) で返却すべし!との注意書きがありました。
ともかく書いてみましょう:
class SAXHandler : public xercesc::HandlerBase {
protected:
virtual void startElement(const XMLCh* const name, xercesc::AttributeList& attributes) {
char* c_name = xercesc::XMLSTring::transcode(name);
std::string s_name(c_name);
xercesc::XMLString::release(&c_name);
if ( s_name == "log" ) {
XMLCh* x_key = xercesc::XMLString::transcode("time");
char* c_val = xercesc::XMLString::transcode(attributes.getValue(x_key));
cout << c_val << ',';
xercesc::XMLString::release(&x_key);
xercesc::XMLString::release(&c_val);
....
うわーめんどくせー…文字列使うたんびにrelease()せんならん、やってらんねーです。
要らなくなったら勝手にまとめてrelease()してくれんもんじゃろか。
...そこでRAII、C++にはデストラクタという強い味方がいます。デストラクタで勝手にreleaseしてくれるヘルパーさんを作っちゃいました。
class TranscodeContext {
public:
~TranscodeContext() {
// 貸出簿に掲載されている全リソースを解放
for ( XMLCh* x_str : xlog_ ) {
xercesc::XMLString::release(&x_str);
}
}
std::string operator()(const XMLCh* str) {
char* c_str = xercesc::XMLString::transcode(str);
std::string result(c_str);
xercesc::XMLString::release(&c_str);
return result;
}
const XMLCh* operator()(const char* str) {
XMLCh* result = xercesc::XMLString::transcode(str);
xlog_.push_back(result); // 貸出簿に載せる
return result;
}
private:
std::vector<XMLCh*> xlog_; // 貸出簿
};
要はTranscodeContextが変換したすべてのXMLCh*を記録に残しておき、デストラクト時にまとめて始末してもらうってスンポーです。
TranscodeContext(と小さなlambda式)を使ってstartElement()を書き直しました。
class SAXHandler : public xercesc::HandlerBase {
protected:
virtual void startElement(const XMLCh* const name, xercesc::AttributeList& attributes) {
TranscodeContext tc;
auto attr = [&](const char* key)
{ return tc(attributes.getValue(tc(key))); };
std::string c_name = tc(name);
if ( c_name == "log" ) {
std::cout << attr("time) << ',' << attr("msg") << std::endl;
}
}
}
...まぁ涼しい。やりたいことが明確でコンパクトなコードになりましたね。
──以上、小さな工夫で楽をするおはなし、2つまとめてお届けしました。
