銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

C++の参照型の落とし穴:クラスメンバに参照型は使わないほうが良さそうだ

先日来、C++のデストラクタで、おかしな動きにぶち当たり「おかしい!バグか?」と大騒ぎした後、最終的に自分のミスに気がつきました。

タイトル通り、クラスにおける参照型に関する落とし穴です。

経緯とともにホントにお恥ずかしい限りですが、忘れた頃に同じことを繰り返しそうなので、恥を忍んで書いておきます。


f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


プログラミング言語C++第4版
ビャーネ・ストラウストラップ Bjarne Stroustrup
SBクリエイティブ
売り上げランキング: 5,351

経緯はこう

  1. クラスのインスタンスメンバに参照型を使ったシンプルなクラスを定義。
  2. そのメンバをデストラクタで使っていたが正しく動作してないようだ
  3. 「デストラクタが動いてないってどういうこと?!」と大騒ぎ(←最初の勘違い)
  4. 落ち着いて、確認用のコードを書いてみたら、デストラクタは動いていた
  5. しかしデストラクタ内では、そのメンバの値が正しくないコンストラクタでは正しかったが?
  6. 「おいおいデストラクタで参照型が使えないってどういうこと?!」と大騒ぎ(←2つめの勘違い)
  7. ここまで全てVisual C++で動かしていたけど、G++では想定通りに動いていた
  8. 「ほうらやっぱり Visual Studio おかしいぞ!」と大声出したら急に自信がなくなって、、、
  9. じっくりコードを眺めてみたら、結局コンストラクタに単純ミスが。。。(自分か)

これが問題のクラス定義だ

スレッド間での排他処理のために、以下の様なクラスを定義したのです。

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

コンストラクタミューテックスをロックして、そのスコープから抜けるときにデストラクタで開放するというものですね。 (※ Visual Studio 2012以降では、スレッド間排他処理のために、CriticalSectionというクラスがあるようですので、そちらを使うほうが良いらしい。それ以前のバージョンではCRITICAL_SECTION構造体を使用。Mutexはプロセス間でも排他処理が可能なロックオブジェクトです)

ところが

以下のように使用しても、ミューテックスが開放されません。 デストラクタ内ではmutexを正しく参照できなくて、どこの馬の骨ともしれないHANDLEを開放している。 なぜだ。

class PacketLogger : Thread {
private:
    HANDLE mutex;
    std::deque<Packet*> packet_queue;
public:
    //   ・
    //   ・
    //   ・
    void AddPacket(Packet* p) {
        MutexLocker lock(mutex); //←ココ
        {
            packet_queue.push_back(p);
        }
    }
    //   ・
    //   ・
    //   ・
}

こうすればちゃんと動いていた

デストラクタで使用するメンバを参照でなく実体にすれば問題なかった

class MutexLocker {
    //HANDLE& mutex; //←参照でなく
    HANDLE mutex;    //←実体に
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

確認コードを書いたはいいがむしろ勘違いを補強する

確認用に以下のコードを書いてみて、Visual Studio の3つのバージョン、2010、2013、2015で確認しても(2012は手元にない)、結果はすべて同じでした。DtorTestClassA のデストラクタでは、コンストラクタで出力した値を出力できない。 最初に書きましたが、MinGW上のG++では、想定通りに動いていましたので、「VisualStudioでは、デストラクタで参照が壊れている」と考えちゃって大騒ぎ。

#include <iostream>

class DtorTestClassA {
    const int& number;
public:
    DtorTestClassA(int number) : number(number) {
        std::cerr << "construction DtorTestClassA #" << number << std::endl;
    }
    virtual ~DtorTestClassA() {
        std::cerr << "destruction DtorTestClassA #" << number << std::endl;
    }
};
class DtorTestClassB {
    const int number;
public:
    DtorTestClassB(int number) : number(number) {
        std::cerr << "construction DtorTestClassB #" << number << std::endl;
    }
    virtual ~DtorTestClassB() {
        std::cerr << "destruction DtorTestClassB #" << number << std::endl;
    }
};
int main(void) {
   DtorTestClassA a1(1);
   DtorTestClassB b1(1);
   {
       DtorTestClassA a2(2);
       DtorTestClassA b2(2);
   }
   for(int i = 0; i < 3; i++) {
       DtorTestClassA a3(i + 3);
       DtorTestClassA b3(i + 3);
   }
}

Visual Studio のバグだろコレ!」って、んなわけないし。

ここで再び、じっくりコードを見なおしてみると、、、別のところがおかしいよと。

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

コンストラクタおかしくないかい?」と。

コンストラクタの引数リストが、参照型になってないよと。

以下のようになっていないとダメじゃないかと?

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE& mutex) : mutex(mutex) { //←ココだ
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

元のコードでは、メンバがコンストラクタのパラメータを参照していて、それってつまりコンストラクタの処理が終われば消滅しているオブジェクトですがな。そらあかんわ。

まとめと所感・教訓および反省文

検証大切

最初のコードを見た段階で気付ける人は幸せです。私は無理でしたが。 気付けないのはまあ、アタマの程度の問題なので仕方がないけど、「ああ、こういうことなんだろうな?」という推測で進んだのがダメダメですな。お恥ずかしい。

人って基本的に、他者を疑うようになっていると思うけど、疑念を公言する前に各方面から確実に検証しなければ、こんな恥ずかしいことになるって事例です。大反省。

未来永劫クラスメンバで参照型は使わない

絶対にそうしなくちゃならないという理由なく、クラスメンバに参照型は使わないほうが良いかもしれないですね。

対象が構造体やクラスオブジェクトの場合はポインタで、完全になんの問題もなく代用可能ですから、そういう理由は今のところ見当たりません。

そもそもなんで最初に参照型にしたのかというと、明確に「そうしなくては」と思ったわけではなく「そのオブジェクトの生成と消滅には直接関わりたくない」という感覚的なものだった。

ところでG++の実装や如何に?

それから、むしろ、G++の動きのほうが怪しいという結果になったけど、もしかすると気を利かせて、こちらの意図に沿うように解釈してくれていたのかも? いや、単にスタックフレームの使い方が違うのかもしれませんね。

無視しないから警告出してよ

いやしかし、自分のミスを棚に上げてでも言っておきたいのだが、クラスメンバがコンストラクタのパラメータを参照しちゃっているのは明らかに間違いなんだから、せめてコンパイル時に警告出してほしいわ。静的解析したら警告なのかな?というか出てた?(←これがよくない)

んなわけないし→デストラクタで参照型のメンバ変数が使えない?

この記事無効です。自分の単純ミスでした。

違う、そうじゃない
違う、そうじゃない
posted with amazlet at 15.12.28
Epic Records Japan Inc. (2013-10-23)
売り上げランキング: 28,637

気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。

takamints.hatenablog.jp

f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


プログラミング言語C++第4版
ビャーネ・ストラウストラップ Bjarne Stroustrup
SBクリエイティブ
売り上げランキング: 5,351

大間違い→デストラクタが自動的に呼ばれない

この記事無効です。自分の単純ミスでした。

違う、そうじゃない
違う、そうじゃない
posted with amazlet at 15.12.28
Epic Records Japan Inc. (2013-10-23)
売り上げランキング: 28,637

気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。

takamints.hatenablog.jp

f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


テキストを日本語的に傍点(圏点)で強調するJavascript

ふと思いついてテキストに傍点をつけるためのスクリプトを作りましたので、ご紹介。(ソースはGitHubに置いてます)

「傍点」は「圏点」ともいうらしいのですが初耳でした。この文書では以降「傍点」で通します。

f:id:takamints:20151128182516j:plain

「傍点によるテキストの強調」は、CSS3のtext-emphasisスタイルで定義されていますが、ブラウザによって対応状況が大きく分かれているようなので、このスクリプトを作った次第。

SafariChrome等、Webkit系の「傍点対応ブラウザ」では素直にそのままCSSで表現しますが、IEFirefoxなどの未対応のブラウザでは「ルビ(RUBYタグ)」を利用して、なんとかしました。

ウェブ上で傍点は、あまり使用することがありませんが、強調というよりも単語の区切りをはっきり示すために使いたい事があります。特に、句読点を入れると、文章のリズムに違和感を感じるような場合などですね。

※ ルビに未対応のブラウザでは、かなりおかしな表示になってしまいますが、傍点よりは対応状況が良いようです。2015年11月の時点で、IEFirefoxの最新版は、全てRUBYに対応してます。Operaプラグインで対応できると聞きましたが、使用していないので詳しく分かりません。


ゲームで学ぶJavaScript入門 HTML5&CSSも身に付く!
田中 賢一郎
インプレス (2015-12-11)
売り上げランキング: 25,808

傍点のスタイル

CSStext-emphasis-styleで指定するのと同じ表現が可能です。

style filled open
dot 横転 好転
circle 争点 当店
double-circle 脳天 法典
sesame 盲点 0点
triangle 栄転 経典

簡単な適用

class='bauten-text-emphasis'となった要素に傍点をつけるのなら、bauten.jsを読み込むだけです。この場合は、黒丸(filled dot)付けられます。

gist.github.com

要素とスタイルを指定する

傍点を適用する要素や、そのスタイルを指定するには、bauten.jsを読み込んだ後のSCRIPTで、bauten関数を呼び出します。

引数は以下のキーを持つオブジェクトです。

キー 説明
className 傍点を打つ要素のクラスを指定します。複数のクラス名をスペースで区切って指定できます。この場合は全てのクラス名を持つ要素が対象になります。
tagName 傍点を打つ要素のタグ名を指定します。
style 傍点のスタイルを指定します。CSSのtext-emphasis-style と同様の指定方法です。
color 傍点の色を指定します。

※ classNameとtagNameのどちらかは指定されなくてはなりません。styleは必須。colorが省略されるとベースの文字色に従います。
※ bauten関数を呼び出した場合、既定の動作は抑制されます。

例:クラス名による指定

gist.github.com

例:タグ名による指定

gist.github.com

任意の文字を傍点にする

任意の文字を傍点にできます。これもtext-emphasis-styleと同様の機能です。

  • たとえばーきみがいるだーけで
  • にわにはにわにわとりがいる。

gist.github.com

傍点の色を指定する

傍点には色を付けられます。これもtext-emphasis-colorと同等。

ブルー・ノート・スケール(ブルース・スケール、blue note scale)は、ジャズやブルースなどで使われる、メジャー・スケール(長音階)に、その第3音、第5音、第7音を半音下げた音を加えて用いるもの、もしくはマイナー・ペンタトニック・スケールに♭5の音を加えたものである。特に、♭5の音をブルー・ノートと呼ぶ。近代対斜の一種でもある。

gist.github.com

リポジトリ

bauten.js は、GitHubの以下のリポジトリで公開しています(MITライセンス)

github.com

SQLiteのSQLのパラメータ化できる箇所

SQLiteSQLiteDatabase.rawQueryで、SQLのパラメタライズが、できる場所とできない場所がありまして、調査結果を記しておきます。

「単なる文字列置換」では無いようです(よくよく考えれば当たり前ですね)。

f:id:takamints:20151104125310p:plain


SQLite入門 第2版
SQLite入門 第2版
posted with amazlet at 15.11.04
西沢 直木
翔泳社
売り上げランキング: 293,597

SQLiteDatabase.rawQuery

概要

単純にSQLをそのまま実行するメソッドSQLのパラメタライズも可能です。

プロトタイプ

Cursor SQLiteDatabase.rawQuery(String sql, String[] paramValues);

引数

  1. sql - SQL文を指定。SELECTのみ。
  2. paramValues - sqlの中の?を置き換える文字列の配列。

使い方例

ざっくり以下のような使い方です。

// android.database.
//     Cursor
//     sqlite.
//         SQLiteOpenHelper
//         SQLiteDatabase
SQLiteDatabase db = openHelper.getWritableDatabase();

Cursor cursor = db.rawQuery(
    "SELECT id, description, amount, date"
    + "FROM t_wallet " /*     v               v   */
    + "WHERE description like ? AND amount >= ?",
        new String[] { "'ポテト%'", "100" });

while(cursor.moveToNext()) {
    int id = cursor.getInt(0);    
    String description = cursor.getString(1);    
    int amount = cursor.getInt(2);
    byte[] blobDate = cursor.getBlob(3);
    //      ・
    //      ・
    //      ・
}

WHERE句の条件にある?を置き換えています。 条件式などの値を置換できます。 しかし単に文字列置換をしているのではなさそうでして、

【できない】文字列リテラルの中身は置換されない

たとえば、description like ?の部分を、description like '%?'とはできません。 あくまでもシングルコーテーションで囲まれている部分は、文字列リテラルとして扱われるので、その中の?は置換されず、単に?で終わっているdescriptionを見つけてきます。ただ、第二引数の要素数が合わない場合は例外が投入されます。

【できる】式の一部の置換はOK

以下のように、式の一部は、置換できます。

rawQuery(
    "SELECT id FROM t_wallet " +
    "WHERE amount >= ? + 30",       /* OK */
    new String[] { "100" })`

※ これ、当初「できない」としていましたが、できました。別の要因で例外が出ていたのかも。すみません。

また、以下のように、単独の値でなくてもOKです。

rawQuery(
    "SELECT id FROM t_wallet " +
    "WHERE amount >= ? + 30",       /* OK */
    new String[] { "(80 + 20)" })`

ところが、次のは駄目でした。「構文エラー」になります。

rawQuery(
    "SELECT id FROM t_wallet " +
    "WHERE amount >= ? ) + 30",       /* NG */
    new String[] { "(80 + 20" })`

?を式として扱って、第1引数のSQL単独で構文エラーになるのは駄目っぽいですね。

【できる】カラム名もOKです

以下のようにSELECT句やWHERE句、ORDER BY句の、カラム名は置換可能でした。

db.rawQuery(
    "SELECT ? FROM t_wallet " +
    "WHERE ? <= ? ORDER BY ? DESC",/* OK */
    new String[] { "description",
                   "amount", "100",  "amount"})`

しかし、

【できない】FROM句のテーブル名はダメでした

予想に反して、テーブル名は置換できないようです。

rawQuery(
    "SELECT description FROM ? " + /* NG */
    "WHERE amount <= 100 " +
    "ORDER BY amount",
    new String[] {"t_wallet"})

そして、

【できない】ORDER BY の DESCもダメですね

これ、できても良い気がしましたが、しっかり例外が投入されました。

rawQuery(
    "SELECT description FROM t_wallet " +
    "WHERE amount <= 100 " +
    "ORDER BY id ?",               /* NG */
    new String[] {"DESC"})`

まとめ

結果をまとめると、以下のようになりました。

置換可能

置換できない

カラム名も構文的には式ですから、?は式を置換できるということで良いのかな。