/ 最近 .rdf 追記 編集 設定 本棚

脳log[20170908] ツールチップ>a.patch | オブジェクトメンバーの持ち方。初期化と再初期化。参照とポインタ。



2017年09月08日 (金) [SakuraEditor] ツールチップがおかしいよね。選択した単語のヘルプがマウスカーソルのそばに出る。そこじゃないよ。■メニュー>設定>キーワードヘルプ自動表示するを選ぶと(※この設定をソースコードで見つけて、どこで設定できるのか探した。iniには書き込まれない揮発性の設定みたいだ)、選択しなくてもキャレット付近の単語の説明が出るんだけど、ポインタの先の単語じゃないんだよね。ポインタのそばに説明が出るのに……。■ポインタがビューの上にないときには表示しない処理もあるんだよね。表示するのはポインタではなくキャレットのそばの単語の説明なのに。■誰かのこだわりの結果なのかなんなのか。普通じゃないね。■2.3.1.0で補完をすると、例えば サクラ を入力してから補完ウィンドウを出して サクラエディタ を選ぶと、サクラサクラエディタ みたいに補完されるのだけど、trunkをデバッグモードでコンパイルしたものがたまたまあるので試してみてもそんなことは起こらないから、何かの設定のせいなのか、よくわからないうちに直ったのか。■単語検索といえば昔のはてなダイアリーは豪快だった。サクラエディタも Trieとか接尾辞配列とかを導入したら、予め決めた単語境界に基づいて抽出した単語を登録済みキーワードと比較するのではなく、もっとアグレッシブに編集中のテキストから登録済みのキーワードを見つけ出せるのに。……とかいって俺には難しすぎるデータ構造だから他人事(ひとごと)だよ。手を動かして理解する手もある?

最終更新: 2017-10-01T01:05+0900

[SakuraEditor] ツールチップ>a.patch

  • ベースは https://svn.code.sf.net/p/sakura-editor/code/sakura -r 4196
  • +1007行, -760行。増えた……
    • CEditView.h, CEditView.cpp, CEditView_Search.cpp, CEditView_Mouse.cpp に限定すると +28行, -429行。圧倒的に減った(ファイルが2つ増えてるんだから多少はね)。
    • 新ファイル(AsWithHelpTooltip.h, AsWithHelpTooltip.cpp)合計が 714行。
    • 目的は減量ではなく機能単位での CEditViewからの切り出しだから……。
  • class CEditViewから消えたメンバー一覧
    • BOOL KeySearchCore(...); publicだが privateにできる。
    • bool MiniMapCursorLineTip(...); publicだが privateにできる。
    • enum LID_SKH {...} publicだが privateにできる。
    • BOOL KeyWordHelpSearchDict(...); publicだが privateにできる。
    • bool ShowKeywordHelp(...); public
    • DWORD m_dwTipTimer; public
    • CTipWnd m_cTipWnd; public
    • POINT m_poTipCurPos; publicだが privateにできる。
    • CDicMgr m_cDicMgr; publicだが privateにできる。
  • 代わりに継承元がひとつ増えた
    • class CEditView: public AsWithHelpTooltip<CEditView>
      • この名前ちょっと微妙? AsSelectable みたいなのを想定した命名規則なんだけど。
    • 基底クラスだけど仮想のデストラクタはない。インターフェイスではないし、public継承されたのもたまたまで、想定される使い方では必要ないはず。
      • 「public継承されたのもたまたま」

        理想を追求する人は private 継承して public 部に using AsWithHelpTooltip::XXX を並べるなり、継承ではなくコンポジションを選び一行だけの委譲関数を書くなりすると思う。というわけで、public 継承している現状に対応して AsWithHelpTooltip::OnTimerAsWithHelpTooltip::OnMouseMove をクラス外部から隠す目的で public から protected に変更するのは間違い。public と private と protected が混在することになるという一事をもって即座に間違いだと気付くべきところ。

      • protected 継承のことは知らない。virtual だとか菱形だとかの継承も難しすぎてわからない。
  • [新機能] メニュー>設定>キーワードヘルプ自動表示する を有効にすると登録キーワードをポイントするだけでヘルプツールチップが出る。
    • その際ツールチップの X座標は単語が始まる場所で、Y座標は注目している行の1.5行下。
    • キャレットを動かしたときも同じ位置に出せるんだけど、そこは互換性。知っていればポインタという定位置にヘルプが出るのも悪くないかもしれないし。
  • [違う所] ビューの境界ギリギリでツールチップを表示したあとポインタをビューの外に持っていくとツールチップが消えないことが本家ではあったがそれがない。
    • ミニマップ(これもビューの一種)のツールチップは必ず消えるんだけど、どういう仕組みによるのかはわからなかった。
    • ミニマップのツールチップはメインメニューが展開しているときにも出る。これは m_bInMenuLoopTRUE にならないからなんだけど、なぜミニマップだけなのか。
  • [気付いた点] CTipWnd::ComputeWindowSize()CTipWnd::DrawTipText() の中で DrawText() の引数 pszWorknew してるけど、DrawTextはサイズを指定する引数も受け取るから _T('\0') を埋める必要も new する必要もないよね。
    • というのが DrawTipText() というひとつの static 関数に反映されてる。

 何がしたかったのか

CEditViewの問題は(ANSI版よりずっと改善したとはいえ)あらゆる機能がひとつのクラスに同居していることなので、publicが privateにできたからといってなんの安心材料にもなりはしない。試したかったのは1機能1クラスで CEditViewを拡張する方法。20091129p01とか20131130。新機能はただの余録。そういうのも簡単にできるよねっていうお試し。

個別機能クラスが結局は CEditViewのすべて(継承元である他の個別機能クラスを含む)に依存できそうなのがちょっと予想外だった。機能と機能の絡み合いがだんだんと窮まってきそう。privateデータメンバー(※)を狭い範囲に閉じ込めて守りやすいのだけが利点か。

※publicデータメンバーもメンバーアクセスを代行するだけのセッタゲッタもありませんよ。それを当然の前提にして privateデータメンバーに注目してる。

 述懐

たまたま目についたひとつのメンバ変数 m_cTipWnd を分離してやろうと思って初めてキーワードヘルプや補完機能をセットアップしてから、コードの絡み合いを解きほぐして再構成するのに一週間ちかくかかってる。これでは、とりあえず一か所に、とりあえず public, friend にしたくなるのもわかる。あえてクラスを分けてアクセスを制限して、その結果に頭を悩ませたい人間がいるだろうか。いやいるし必要なんだけど、結局自分の能力でできる範囲のやり方しかできない。この a.patch だって自信満々で晒してるわけではなくて、むしろクラス設計は迷いまくりで、AsWithHelpTooltip.GetLastHitKeywordDictionary() なんて行き場のない妥協の産物である。正しい設計なんてのは与えられればそれ以外に考えられないくらい当たり前のものに思えるのに、自分でそこにたどり着くのは難しかったりする。高校数学の問題とかがそう。解法をチラ見すれば解ける。しかし独力ではなんのとっかかりも見つけられない。

最終更新: 2017-11-04T18:59+0900

[C++] オブジェクトメンバーの持ち方。初期化と再初期化。参照とポインタ。

クラスを書くときの理想として、コンストラクタでメンバーを初期化して以後は変更したくないというのがある。メンバーの状態は自身の行動の前提条件であり、そこが流動的ではあらゆるメソッドが事前に前提を確認してからでないと本題に入れなくなる。コンストラクタで準備の完了と条件の確認をすべて済ませておきたい。そこが動く、動いたなら、それはパラメータを変えた別のインスタンスに任せたい。

上で登場した AsWithHelpTooltip というオブジェクトは CTipWnd というオブジェクトをメンバーとして持っており、寿命を共にする。CTipWnd のサイズ(を始めとするその他諸々)を隠したいならポインタ(か参照)一択だが、そういう制約がないなら new CTipWnd の手間を避けて、AsWithHelpTooltip のサイズに CTipWnd のサイズを丸々含めるような持ち方をする。

初期化である。AsWithHelpTooltip とそのメンバーである CTipWnd は As~を継承する CEditView の初期化(ウィンドウ作成など)を待たないと初期化が完了しないので、As~のコンストラクタでは呼ばれるのが早すぎる。また、CEditView はコンストラクタとは別に Create という名前の初期化(リセット)関数を持っている。であれば As~にも Create という名前の初期化関数を生やして CEditView の Create から呼んでもらうというのが自然な流れ。As~はそれでいい。そのメンバーである CTipWnd にも Create の連鎖を波及させるべきだろうか。CTipWnd は小さな部品であり、そこそこ汎用性もある。そこに、外部の都合で、リセット関数という全く不要で全ての前提を覆す邪悪なメンバーを生やしてもいいものだろうか。

代入演算子を定義しようか。しかし代入は邪悪で避けるべきものだ。なにより外部のリソースを作成するときに使用したパラメータは、コンストラクタの中で使用するのみで以後は不要なので保存していない。代入なんかのためだけに余計な荷物をしょいこむなんてまっぴらごめんである。

ではやはりポインタだろうか。Create 関数が呼ばれるたびに new CTipWnd でメンバーのインスタンスを作り直そうか。こういうとき参照は使えない。参照はコンストラクタで初期化するしかできないし、初期化しなければいけない。そこが参照のポインタとは違う参照たる所以であるので不便でも不満でもない。しかしポインタには不満がある。ポインタは無効な状態(nullptr)を許容する所が参照とは違う点で、今回の As~と CTipWnd の関係ではそういう無効な状態の入り込む余地などないのだから、CTipWnd をポインタで持つというのは意図や前提の過不足のない表現として不適切で不満がある。

結局 As~の Create 関数の中でこうやって CTipWndを再作成したんですよ。

m_cTipWnd.~CTipWnd();
new(&m_cTipWnd) CTipWnd( G_AppInstance(), asView(this)->GetHwnd() );

代入なし、(CTipWndに)リセット関数なし、new なし、ポインタなしで満足してるんだけど、何かの本で読んだテクニックというわけではなくて、どんな落とし穴があるのか戦々恐々としてる。普通の new ですら使うのが怖くてできるだけ見えないように(STLを使ったり)するのに、placement new なんてとてもとても。

 @2017-09-28 placement new より swap

picojsonというパーサを読んでいます。分からないのは、以下に該当するコードです。deleteしていないのに、配置newしたオブジェクトがメモリリークしないのはなぜなのでしょうか。

inline value& value::operator= ( const value& x ) {
    if( this != &x ){
        this->~value();
        new (this) value(x); // thisの寿命はどうなる?
    }
    return *this;
}

ほぼ同じ2行だ。そして picojson は知っているし信頼できるソースだ。だけど検索したら現在の picojson にはその2行がない。こういうことらしい。

make operator= safe when part of LHS is being assigned, as well as ex… · kazuho/picojson@96f6c81

exception-safe はわかる。破壊と構築がアトミックでないから、構築し損ねた荒れ地にさらにデストラクタが走る可能性がある。

でも when part of LHS is being assigned っていうのがわからない。さへんのいちぶぶんにだいにゅうされているとき?

 @2017-10-30 placement new より swap (続き)

c++ - Can I use placement new(this) in operator=? - Stack Overflow

ある回答者によれば『Exceptional C++』で言及されているらしい。もう一度読もうか。

その回答に補足して、継承とスライシングに関わる問題が placement new を使った手法にはあるとも指摘されている。picojson の主な理由もそれだったのかもしれないけど、よくわからん。

ある型の代入演算子の中で自身のデストラクタを呼ぶとき、それが仮想であるならば呼ばれるデストラクタは派生クラスのものであるかもしれず、placement new で新しいインスタンスで上書きするときに派生部分の初期化が行われない、と。そのようにコードを呼んだ側も呼ばれた側も意図するところは「ある型」部分に限った代入なので、仮想のデストラクタを呼んだのが間違い。

『[単行本(ソフトカバー)] Jaroslav Tulach【APIデザインの極意 Java/NetBeansアーキテクト探究ノート】 インプレス』を読んでその苦労を垣間見るけど、派生クラスは難しい。派生が可能なクラスを公開して、それを互換性を保ったまま改善するというのは、ほとんど不可能なのではないかと思えるほど。どれだけ注意を払っても余分なコードで対策しても、得られるのは「ソースコード上の」とか「バイナリの」とかいう条件付きの互換性なんだからやってられない。

 @2017-11-02 placement new より swap (続きの続き)

『Exceptional C++』の項目41がそのものずばりでアンチイディオムとして取り上げており、すでに述べた2つ以外にもこれでもかこれでもかと否定の論拠を挙げていた。C++怖い。そして、「何かの本で読んだテクニックというわけではなくて」とか書いていたのは誰だっけ?

実際のところ、a.patch のケースでは例外安全性以外の問題はないと思うんだよね。Exceptional C++ のケースも picojson のケースも、継承を伴う代入演算子の話だし。問題があるのはたしかで万事うまくいく解決策もあるのに、あえて危険な手段をとったのが知識と思慮の不足ゆえなのは否定しようがないけど。