C++の参照型の落とし穴:クラスメンバに参照型は使わないほうが良さそうだ
先日来、C++のデストラクタで、おかしな動きにぶち当たり「おかしい!バグか?」と大騒ぎした後、最終的に自分のミスに気がつきました。
タイトル通り、クラスにおける参照型に関する落とし穴です。
経緯とともにホントにお恥ずかしい限りですが、忘れた頃に同じことを繰り返しそうなので、恥を忍んで書いておきます。
photo credit: The Arm of Destruction via photopin (license)
経緯はこう
- クラスのインスタンスメンバに参照型を使ったシンプルなクラスを定義。
- そのメンバをデストラクタで使っていたが正しく動作してないようだ。
- 「デストラクタが動いてないってどういうこと?!」と大騒ぎ(←最初の勘違い)
- 落ち着いて、確認用のコードを書いてみたら、デストラクタは動いていた
- しかしデストラクタ内では、そのメンバの値が正しくない。コンストラクタでは正しかったが?
- 「おいおいデストラクタで参照型が使えないってどういうこと?!」と大騒ぎ(←2つめの勘違い)
- ここまで全てVisual C++で動かしていたけど、G++では想定通りに動いていた。
- 「ほうらやっぱり Visual Studio おかしいぞ!」と大声出したら急に自信がなくなって、、、
- じっくりコードを眺めてみたら、結局コンストラクタに単純ミスが。。。(自分か)
これが問題のクラス定義だ
スレッド間での排他処理のために、以下の様なクラスを定義したのです。
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++の動きのほうが怪しいという結果になったけど、もしかすると気を利かせて、こちらの意図に沿うように解釈してくれていたのかも? いや、単にスタックフレームの使い方が違うのかもしれませんね。
無視しないから警告出してよ
いやしかし、自分のミスを棚に上げてでも言っておきたいのだが、クラスメンバがコンストラクタのパラメータを参照しちゃっているのは明らかに間違いなんだから、せめてコンパイル時に警告出してほしいわ。静的解析したら警告なのかな?というか出てた?(←これがよくない)
んなわけないし→デストラクタで参照型のメンバ変数が使えない?
この記事無効です。自分の単純ミスでした。
売り上げランキング: 28,637
気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。
photo credit: The Arm of Destruction via photopin (license)
大間違い→デストラクタが自動的に呼ばれない
この記事無効です。自分の単純ミスでした。
売り上げランキング: 28,637
気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。
photo credit: The Arm of Destruction via photopin (license)
テキストを日本語的に傍点(圏点)で強調するJavascript
ふと思いついてテキストに傍点をつけるためのスクリプトを作りましたので、ご紹介。(ソースはGitHubに置いてます)
「傍点」は「圏点」ともいうらしいのですが初耳でした。この文書では以降「傍点」で通します。
「傍点によるテキストの強調」は、CSS3のtext-emphasis
スタイルで定義されていますが、ブラウザによって対応状況が大きく分かれているようなので、このスクリプトを作った次第。
Safari、Chrome等、Webkit系の「傍点対応ブラウザ」では素直にそのままCSSで表現しますが、IEやFirefoxなどの未対応のブラウザでは「ルビ(RUBYタグ)」を利用して、なんとかしました。
ウェブ上で傍点は、あまり使用することがありませんが、強調というよりも単語の区切りをはっきり示すために使いたい事があります。特に、句読点を入れると、文章のリズムに違和感を感じるような場合などですね。
※ ルビに未対応のブラウザでは、かなりおかしな表示になってしまいますが、傍点よりは対応状況が良いようです。2015年11月の時点で、IE、Firefoxの最新版は、全てRUBYに対応してます。Operaはプラグインで対応できると聞きましたが、使用していないので詳しく分かりません。
傍点のスタイル
CSSのtext-emphasis-style
で指定するのと同じ表現が可能です。
style | filled | open |
---|---|---|
dot | 横転 | 好転 |
circle | 争点 | 当店 |
double-circle | 脳天 | 法典 |
sesame | 盲点 | 0点 |
triangle | 栄転 | 経典 |
簡単な適用
class='bauten-text-emphasis'
となった要素に傍点をつけるのなら、bauten.js
を読み込むだけです。この場合は、黒丸(filled dot)付けられます。
要素とスタイルを指定する
傍点を適用する要素や、そのスタイルを指定するには、bauten.jsを読み込んだ後のSCRIPTで、bauten関数を呼び出します。
引数は以下のキーを持つオブジェクトです。
キー | 説明 |
---|---|
className | 傍点を打つ要素のクラスを指定します。複数のクラス名をスペースで区切って指定できます。この場合は全てのクラス名を持つ要素が対象になります。 |
tagName | 傍点を打つ要素のタグ名を指定します。 |
style | 傍点のスタイルを指定します。CSSのtext-emphasis-style と同様の指定方法です。 |
color | 傍点の色を指定します。 |
※ classNameとtagNameのどちらかは指定されなくてはなりません。styleは必須。colorが省略されるとベースの文字色に従います。
※ bauten関数を呼び出した場合、既定の動作は抑制されます。
例:クラス名による指定
例:タグ名による指定
任意の文字を傍点にする
任意の文字を傍点にできます。これもtext-emphasis-style
と同様の機能です。
- たとえばーきみがいるだーけで
- にわにはにわのにわとりがいる。
例
傍点の色を指定する
傍点には色を付けられます。これもtext-emphasis-color
と同等。
ブルー・ノート・スケール(ブルース・スケール、blue note scale)は、ジャズやブルースなどで使われる、メジャー・スケール(長音階)に、その第3音、第5音、第7音を半音下げた音を加えて用いるもの、もしくはマイナー・ペンタトニック・スケールに♭5の音を加えたものである。特に、♭5の音をブルー・ノートと呼ぶ。近代対斜の一種でもある。
例
リポジトリ
SQLiteのSQLのパラメータ化できる箇所
SQLiteのSQLiteDatabase.rawQuery
で、SQLのパラメタライズが、できる場所とできない場所がありまして、調査結果を記しておきます。
「単なる文字列置換」では無いようです(よくよく考えれば当たり前ですね)。
SQLiteDatabase.rawQuery
概要
単純にSQLをそのまま実行するメソッド。SQLのパラメタライズも可能です。
プロトタイプ
Cursor SQLiteDatabase.rawQuery(String sql, String[] paramValues);
引数
使い方例
ざっくり以下のような使い方です。
// 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"})`
まとめ
結果をまとめると、以下のようになりました。
置換可能
- カラム名
- 値
置換できない
カラム名も構文的には式ですから、?
は式を置換できるということで良いのかな。