2022年の振り返り

仕事関連

個人事業主一年生存

メインの取引先と切れていないのでサバイバルしたぜ感はまったくないですが、つつがなく一年生存しました。安定は良いこと。

特に仕事を詰め込もうとはしていませんが、メインの仕事+αな感じで、頼まれたら空き時間にお手伝い的な仕事を増やしたりはしました。

やってみて感じたのはどうせやるなら成長につながる仕事がいいなぁということ。ITの仕事なんてのはいつも日本のどこかで火を噴いていて、私のスキルが役に立つ場面というのはそりゃああるわけですが、手持ちのスキルを適用していくだけである程度の改善を見込める仕事というのはいまいち成長につながらず、時間を売ってお金に換えてるだけな気がしてしまうんですよね。

仕事をいただけること自体はありがたいことですし手伝って欲しいと言われて邪険にしようとは思わないですが、こちらから頭を下げてでも入りたいという仕事も見つけていかねばと感じました。

メインのお仕事について

モバイルゲーム、リリースから一年たちましたがまあいろいろありました。最初の半年は「これ直さないとサービス終わるぞ」レベルのやつをずっと直してましたね。最近になってようやくやりたいこと(チューニング)に時間をさけるようになってきたので、どんどんユーザーが快適に遊べるようになってくると思います。次の半年である程度出し切れればいいなと思っています。本プロジェクトにおける私の個人目標は十分速いアプリを作ることでしたので、最低限そこまでやらないと数年間捨てたことになりますからね。半端なところでは投げ出せない。

「ここまで作りこまないとゲームとしてまともな性能はでないんだぜ」という基準をきちんと自分の中で確立すること。確立した基準でもって基盤をととのえ、ドキュメントを整備し、他のメンバーの緩いコードに対しては否と言えること。なあなあではいけません。自分の基準に自信がないと「このコードは遅いかもしれないが一旦これで行ってみてダメだったら直そうか」なんて曖昧な判断が増えてしまいます。それが増えるほど終盤の修正では手がまわらなくなります。

リリースしたそれなりのランキングのタイトルの保守運用に関われるというのは、特に性能面の経験を積むのに非常に価値があります。最大限に活かしたいところです。

技術

2021年の振り返りで書いたことがまったくできていない衝撃…いや、触りはしたんだけど思ったより手触りが悪かったというか時期尚早な気がするものが多かったんですよね。Visual Scriptingとか。一日触って放り投げてしまった。

Unity ECSはversion1.0に向けて大きな動きがありましたね。まだExperimentalではありますが、実験プロジェクトではなくちゃんとリリースへの道筋を見せてきたことは評価したいと思います。1.0向けの機能セットも出そろったので学ぶには良い時期です。私がサボっているだけで。

Blazorまわり。全然やってネェ。切腹

なんだかんだで一番触ったのはC++/WinRTとかWinUIな気がします。こちらはv1.2がリリースされUWPからの移行先としての機能は出そろいつつありますね。Content Island(XAML Island)周りがまだExperimentalですが、さすがにv1.3には入るのではなかろうか。

そこまで来たら次はWPFからの移行先としてふさわしい機能が増えてくるはずです。v1.4(2023/11ぐらい?)で片鱗が見えるといいなと思っています。はたしてWindowsのクライアントUIフレームワークの長い混乱を収めることができるのでしょうか。インストーラの品質がひどいのも直してくださいね。

生活、健康面

片手20kgの可変重量のダンベルx2を5月に買ってからぼちぼち筋トレを続けております。腕や胸はすこしずつゴツくなってきました。1年で体重はあまり落ちなかった(-5kg)のですが、健康診断の数値は大きく改善されたので効果はあったのでしょう。来年は月1kgぐらいのペースで体重を落としたいですね。目指せ標準体重。

生活リズムはまたちょっと悪化してきました。慣れって怖い。仕事には悪影響ないようにしていますがもうちょっと朝型になりたい。

趣味

ピアノ丸二年を過ぎて成長鈍化を実感。中級者への壁みたいなものを感じます。ちゃんとした先生に師事するのが良いのでしょうけどうちにあるのMIDIキーボードだしな… もともとの目標である作曲に役立つ最低レベルの演奏力というのは達成しているので、真面目にやるかは悩み中です。

9月に自分への誕生日プレゼントということでエレキギターを買いましたが、夜に音鳴らせないので頻度は微妙。シールド挿すところが硬くて個体の不良っぽく、PCのアンプシミュに挿して引いてみても7khzより上が出なくて抜けが悪く、なんか変なところがあるんだろうなという感じ。扱いが難しい子になってしまっています。そのうち修理にだしてみようかな。

作曲の真似事というか、メロディを作ることができるようになってきたのでDAWを触る時間が増えました。メロディを作れるようになると、今度は編曲とかミックスとかマスタリングとかいろいろあって音楽は奥が深いのだなと遠い目になっているところです。Black Fridayでソフトは十分すぎるほど整ってしまったので、来年は学びながらなんか出せるといいなと思っています。

2023年もよろしくお願いします。

SampleTank4の不具合

2022/12/29 SampleTank4 version 4.2.3時点で私が遭遇した不具合です。 IK multimedia のサポートには連絡済み。 そのうち修正されることを期待します。

当環境

Windows 11 22H2

DAWでSampleTank4を使ったプロジェクトを開く際にSampleTank4.vst3がエラーで落ちる

これは、10GBを超える特定の音色を使用していると発生します。

回避方法

単に上記音色を使わなければOKです。ものによっては、軽量版であるところのSE音色がありますのでそれを使えばよいでしょう。

  • C7 Grand Binaural - Natural SE (3.2GB)
  • C7 Grand Close Mic - Natural SE (3.1GB)
  • C7 Grand Coincident Mics - Natural SE (3.1GB)

発生すると該当のSampleTank4のインスタンスを消してから立ち上げなおしになりますので、音色やエフェクトはすべて設定しなおしとなり、なかなかのストレスですが回避が容易なのでさほどきつくありません。C7 Grand Binaural - Naturalは好きな音色なので気兼ねなくリッチなほうを使えるようになると嬉しいなぁとは思っています。

音色切り替え時に稀にSampleTank4が落ちる

発生確率は低いですが、すでに音色をロード済みのPartに別音色を読み込み、つまり切り替え時にSampleTank4が落ちることがあります。

回避方法

私の知る限りではありません。

回避できないですが、Studio Oneはこのエラーが発生しても踏ん張ってくれるのでさほど困っていません。発生してもちょっとウザい程度です。 DAWによっては発生時にDAWごと死ぬので結構なストレスになるかもしれません。

余談

IK multimediaのフォーラムにもお怒りの方がいらっしゃるので、私の環境だけではなさそう。 https://cgi.ikmultimedia.com/ikforum/viewtopic.php?f=12&t=23636#p100238

SampleTank4世代の音色はROUND ROBIN表示が逆である

Edit画面でROUND ROBINのON/OFFができますが、SampleTank4世代の音色は表示が逆なバグがありそうなので注意が必要です。 ROUND ROBINがあるとウリになるはずのドラム音色で初期値OFFなのでわけわからんなと思っていたらただの表示バグっぽい。 SampleTank3世代のドラム音色はROUND ROBINがONだったので、もしかして4になって劣化したかと心配しましたが、実サウンドのほうは問題なさそうです。

DAWでSampleTank4を演奏したときにプチノイズが発生

これはSampleTank4の不具合ではなく私のオーディオ設定の問題でした。 SampleTank4は全然悪くないのですが、参考になる人がいるかもしれないので記載しておきます。

SampleTank4の推奨バッファは最低でも1024です。

この値をオーディオインターフェースに設定することで直りました。単に私が切り詰めすぎていただけなのです。

一部音色をロードできない

SampleTankというよりはIK Product Managerによるサウンドのインストールに問題があると発生します。 インストールの不備に関しては別途記載しています。

enrike3.hatenablog.com

Total Studio 3.5 MAXのインストール

Black Fridayなので、評判がよくて長く使えそうなMODO BASS 2を買ったのですが、そこから

「Amplitube 5 MAX 87%オフ!ギターも買ったことだしアンプシミュも持っておくか!」

「え、MAX製品もってたらTotal Studio 3.5 MAXも87%オフなの?」

と87%OFFの連鎖につられて気が付けばTotal Studio 3.5 MAXを丸っとMy New Gearしてしまっていました。IK商法おそろしい…… なお先にTotal Studio 3.5 Maxを買ってからMODO MAXでMODO BASS 2を買うのが最安だった模様。これがDTM界隈の黒金か。

というわけで大量のソフトウェアを手に入れてしまったのですがインストールがすんなりはいきませんでした。

同じように激安黒金に釣られてTotal Studio MAXを買ったけどいまいちちゃんと動作しない、という人が他にもいるかもしれないのでここにログを残します。

私の環境だけで上手くいかない(いわゆる「おま環」)ということもあるでしょうから、この記事をもって製品を批判する意図がないことは明記しておきます。

この記事の要約

  • webのMy Productをみて、IK Product Managerが落としたファイルが不足ないか確かめよう
  • LibraryのUpdateを当てよう(SampleTank4 / Miroslav Philharmonik2)
  • Syntronik2については、不足ファイルを手動ダウンロードすることはあってもそのインストールはIK Product Managerに任せよう

当方の環境

Windows 11 22H2

IK Product Manager でインストール

インストールとAuthorizeは基本的にIK Product Mangerを使います。 ダウンロードするものはソフトウェア本体とサウンドファイルですが、ソフトウェア本体のダウンロードとインストールはIK Product Manger経由で全く問題はありませんでした。

困ったのはサウンドファイルのほうで、SampleTank 4 MAXとSyntronik 2のサウンドファイルが一部インストールされませんでした。IK Product Mangerは正常にインストールされたと言い張るのですが、実際にSampleTank4やSyntronik2のStandalone版を立ち上げて音色を読み込もうとすると読み込めない奴がでてきます。VST3として使っても同様です。

修正方法

サウンドファイルは、IK multimedia公式サイトにログインして、My Product→該当製品→Souds Downloadをたどることで手動で落とすことができます。

このWebからのダウンロード一覧と、IK Product Mangerがローカルに落としたzipの一覧を突合して不足分は手動でダウンロードします。 ローカルのダウンロードパスは、たとえばSampleTank4 Maxであれば、

%UserProfile%\Documents\IK Multimedia\IK Product Manager\SampleTank 4 MAX

にあります。IK Product Manager上でダウンロード先を変更している場合はそちらを参照します。

私のケースでは、以下のファイルを手動で落とす必要がありました。

SampleTank4 - SampleTank_3_Sound_Content_Part_5.zip - SampleTank_3_Sound_Content_Part_6.zip

Syntronik2 - SYN で始まるファイル多数 (SYN2のほうは大丈夫だった)

zipを全量揃えたら、IK Product Manager上でre-installします。おそらくこれで直るはずです。

LibraryのUpdateについて

webのMy Product → SampleTank4 Max → Sounds Library Updateに、音色のアップデートパッチ(2022/12/29時点ではUpdate 1.5)があります。 多くの木管や弦などの音色にKeySwitchの使いやすいやつが増えるのでこれは適用したほうが良いものですが、 おそらくですが、IK Product Manager経由でこれのパッチを当てる方法はありませんので手動でやることになります。私は手動でやりました。

Miroslav Philharmonik 2のほうにも、Download ResourcesのほうにLibrary Updateがあります。なぜ統一されていないのか… これも手動であてました。

Syntronik2の手動インストールについて

サウンドのzipファイルの中にはインストーラのexeが同梱されています。

基本的にはこのexeをたたけば手動でインストールしていくことが可能です。 ただし、Syntronik2に関しては手動インストールの難易度が高いです。 Syntronik2は、製品の構成としてSampleTank3世代とSampleTank4世代のハイブリッドになっているため、インストーラのインストール先が分裂しがちなのです。

といってもぱっとは理解しにくいでしょうから詳細に入ります。

SampleTankはIK multimediaの他の製品をSampleTank内で扱える

SampleTankはIK multimediaの他の製品をSampleTank内で扱えます。具体的には以下です。

  • Miroslav Philharmonik2
  • Syntronik2
  • SampleTron2

これら、他の製品をSampleTank内に取り込む仕組みが、SampleTank3世代とSampleTank4世代で異なります。

SampleTank4世代の他製品取り込み

SampleTank4の中のSoundContentの中に、取り込む製品のパスを追加します。(つまり複数のパスを持てる) これらのパスはレジストリの「HKEY_CURRENT_USER\Software\IK Multimedia\SampleTank 4」の最大32まで持てるLibraryPathの中に保存されます。

SampleTank3世代の他製品取り込み

SampleTank3世代の製品は、SampleTank4と違いサウンドのLibraryPathを一つしかもてません。 レジストリの「HKEY_CURRENT_USER\Software\IK Multimedia\SampleTank 3」の中をみても1つのパスしかないはずです。

つまり、複数製品を取り込むには、同じフォルダにコンテンツを入れるしかありません。

例としてMiroslav Philharmonik 2があります。これはSampleTankの拡張音源ではありませんが、連携のためSampleTank3のフォルダに入ります。

Syntronik2は二世代ハイブリッド

Syntronik2は、初代Syntronikの音資産(SampleTank3世代)と新規パッチ(SampleTank4世代)が混ざった世代ハイブリッド製品となっています。 これはInstrumentsの拡張子をみるとわかります。

Factoryは初代Syntronikの音色ですが、拡張子がst3iとなっています。これはSampleTank3Instrumentを表します。

Syntronik2フォルダは、名前の通りSyntronik2世代の音色が入っていて、拡張子はst4iです。

この2世代のファイル群は、ダウンロードするzipファイルの時点で分かれています。下記画像の左右のファイル群は同じフォルダに入っています。 SYN_xxx.zipが初代Syntronikの、つまりSampleTank3世代のzip、SYN2_xxx.zipがSyntronik2の、つまりSampleTank4世代のzipです。

SYN_xxx.zipの中のインストーラーはSampleTank3世代なのでファイルをSampleTank3フォルダに入れようとしますが、SYN2_xxx.zipの中のインストーラーはSampleTank4世代なので、ファイルをSyntronik2フォルダに入れようとします。

これがSyntronik2の手動インストールが難しい理由です。何も知らずにやるとインストール先が割れてしまいます。

IK Product Managerはここをちゃんと処理しますから、Syntronik2に関しては、不足ファイルを手動ダウンロードすることはあってもインストールはIK Product Managerに任せたほうが良いでしょう。

おまけ:Miroslav Philharmonik 2 CEを消したい

Miroslav Philharmonik 2 CEは、Miroslav Philharmonik 2の音色削減版で、SampleTank4 MAXに含まれます。 Total Studio 3.5 MAXにはMiroslav Philharmonik 2が含まれるので、SampleTank4 MAXとMiroslav Philharmonik 2をインストールするとCEの音色は重複します。 これを消したい。

SampleTank 3 のインストールフォルダにあるMiroslav Philharmonik系のファイル、フォルダを片っぱしから消して、Miroslav Philharmonik 2のサウンドをre-installするのがベストです。(re-install後はLibrary Updateも忘れずにどうぞ)

InstrumentsやSamples、Library Infoは、CEとCEじゃないほうが明確に分かれていますが、Patternsフォルダの中身はCEとCEじゃないほうのファイルがごっちゃになってしまっています(CEフォルダに分離されていない)。同名のファイルすらあるので、Patternsだけは、CEのファイルだけを綺麗に消すことは不可能です。

PatternsもCEフォルダを作って、CEのファイルはそこに入れてくれるようにサポートに要望は出しました。

HSTRING完全に理解した

HSTRINGというのはUWPやWindowsAppSdkで使われる、Windows Runtimeの文字列です。 Windows APIでは文字列の種類がいろいろあります。LPTSTRやらBSTRやら……これらの新顔で、オーバーヘッド大き目のかわりに柔軟です。

HSTRINGは基本的には<winstring.h>ヘッダーに含まれる WindowsCreateString APIで作成します。

HSTRING str = nullptr;
auto hr = ::WindowsCreateString(L"hoge", 4, &str);

このときヒープに以下のようにメモリが確保されます。

HSTRING_HEADERは、hstring.hに以下のように定義されています。 内部にポインタがあるので、32bit環境と64bit環境ではサイズが異なります(20byteと24byte)。

// Declare the HSTRING_HEADER
typedef struct HSTRING_HEADER
{
    union{
        PVOID Reserved1;
#if defined(_WIN64)
        char Reserved2[24];
#else
        char Reserved2[20];
#endif
    } Reserved;
} HSTRING_HEADER;

byte数だけを定義しているようなものなので、画像内のHSTRING_HEADER内のレイアウトやメンバ名は私が勝手につけたものになります。正確さは保証しません。

HSTRINGを使い終わったら、WindowsDeleteString APIで参照カウントを減らします。参照カウントが0になったら削除されます。

メモリレイアウト的には、HSTRINGHSTRING_HEADER構造体へのポインタであり、HSTRING_HEADER内のptrRawBufferポインタがWCHAR*のNULL終端文字列を指していれば成立します。 よって、プログラマが任意の場所にHSTRING_HEADERとWCHAR*のNULL終端文字列分のメモリを確保すれば作成することができます。 スタック、つまりローカル変数を用いてもOKです。スタックを用いる場合はヒープにメモリを確保するコストがないのでfast stringなどと呼ばれるようです。 メモリを自前で管理してHSTRINGを作成するには、WindowsCreateStringReference APIを使用します。

wchar_t raw[5] = L"hoge";
HSTRING_HEADER header{};
HSTRING str = nullptr;
hr = ::WindowsCreateStringReference(raw, 5, &header, &str);

bFastStringのフラグがたって1になり、HSTRING_HEADERとRawBufferが不連続となり、WindowsCreateStringのときは間にあった参照カウント領域が存在しません。

WindowsCreateStringReferenceは渡されたメモリがどのように確保されたかを知りませんから、WindowsDeleteStringが適切にメモリを解放することは不可能なので、そもそも管理できないなら参照カウントもなくしてしまえということでしょう。実際にWindowsCreateStringReferenceで作成されたHSTRINGをWindowsDeleteStringに渡しても何もおきません。HSTRING_HEADERとRawBufferのメモリについてはWindowsCreateStringReference呼び出し側が解放の責務を負います。スタックを用いた場合はもちろん明示的な解放は不要です。

スタックから作成したHSTRINGを戻り値で返したい場合は、解放されてしまうのでそのまま返すわけにはいきません。その場合はWindowsDuplicateString APIで文字列を複製します。WindowsCreateStringReferenceで作成されたHSTRINGを複製すると、同内容のものがWindowsCreateStringで作られたときと同じようにヒープに作成され、参照カウントで管理されます。

HSTRINGはImmutable、つまり不変なのでWindowsCreateStringで作られたHSTRINGに対してWindowsDuplicateStringを呼ぶと、複製が作られずに参照カウントだけが増えます。WindowsCreateStringReferenceで作成されたHSTRINGの場合は、寿命管理外のものを寿命管理下のものに変更するためにコピーをしますが、もともと寿命管理下のものはコピーする意味がありません。

ボタンの命名は連番!?

Twitterで話題になったこちらについて。

「用途別ではなく機械的に名前をつけるべき」という内容に対して各所から非難轟々だったのですが「ちゃんとした名前つけるのが当然だろ」みたいな反論ばかりだったのが気になりました。

そもそも用途と見た目は独立して取り扱うべきものです。

「一つの用途のUI部品に多数の見た目があるとユーザーが混乱するので避けるべき」という一般論があるのは理解していますが、それでも現実の問題として見た目にはバリエーションがあります。スマホゲームにおいてその最たるものは画面遷移ボタンだと思います。グローバルメニューにあったりヘッダーフッターにあったり、画面内のボタンにあったりし、また重要機能(ガチャやメインコンテンツなど)への導線の見た目が盛られたりと、とても多様になりがちです。

逆に、一つの見た目がさまざまな用途のボタンに割り振られることもあります。

f:id:enrike3:20220222173006p:plain

この図はプリコネから抜粋した例ですが「入手方法」と「キャンセル」が同一用途だと言い張る人はまずいないでしょう。汎用系のスタイルは多用途に横展開されるのが常です(それが汎用系ということですからね。トートロジー)。

冒頭のTwitterの例をみてみると、「用途別」の命名を独立した要素であるはずの「見た目」の問題で否定しているのがそもそもの間違いで、二つの世界が混ざってしまっています。見た目の横展開が楽な見た目都合の連番方式というのは、ロジックを組む用途側の都合を考慮していないということで、変数名であったりコーディング側の感覚で反論が来るのです。

もし「青ボタンスタイル」の見た目のボタンを用途別にOKButton, SubmitButton、SceneTransitionButton…として別々にアセットを作っていたら、デザイナさんが「青ボタンスタイルを更新したい」というときに全部直す必要がでて大変です。当たり前ですがデザイン側は一元管理したいのであり、そのニーズを一方的に無視するわけにはいきません。一方で見た目単位のアセットを正とし、BlueButton1の単位でアセットを各シーンに配置してしまうと、OKButtonだけスタイル差し替えたい、というときに全箇所修正が必要ということになって非常に厳しいことになります。

見た目ボタンと用途別ボタンはどちらも別々に作り、用途別ボタンに見た目を割り当てるということができればベストです。WPFなどのフレームワークならBasedOnプロパティを使ってStyle継承ができますので見た目重視のStyleを継承して用途単位でサブクラス化できます。Unityの場合UI向けのStyle機構がないので、用途別ボタンのPrefabに見た目Prefabを組み込むとか、ツールを使ってスタイル一括適用とかになるでしょうか。とはいえ用途別も見た目別も全部作るなら管理するアセット数は最大になるので、どちらか片方だけを使うというプロジェクトも多いはずです。

さて、見た目ボタンを作るなら命名は連番とちゃんとした名前付けどちらが好ましいのかですが、用途から切り離して名前を付けるというのはなかなか難しいところがあります。物理的なプロパティをかき集めてbutton-blue-frame02-r16 みたいな方法が考えられますがデザイン変更したら名前が変わることになるので変更に弱く、結局button-type01, みたいな連番と大差ないものに落ち着くと思います。テーマやアクセントカラーがはっきりしたデザインならそれらを名前に使うこともできるでしょう。もちろんアクセント1みたいな名前付けをしたとしてもこれは色に対するエイリアスに過ぎず、用途別命名とは程遠いものになります。

「ああ、この人は見た目管理の世界に軸足を置いているんだな」というところが分かればそこまで叩かれるような記事とは思わないのですが、OKボタンの見た目をキャンセルボタンに使うことがあるから、のあたりはさすがに不味かったですかね。

(連番→機械的、に一部修正)

EFCoreに深入り(DbContextをnewするあたり)

2/22追記、この記事の内容の応用としてPomelo.EFCoreのシャーディング対応をgistにあげてみました Pomelo.EFCoreでSharding · GitHub


この記事は、以下のコードを読んでみた結果です。 github.com 当記事内のコードはどれも簡略化したものなので本当の実装が気になる型は上記公式リポジトリのコードを確認してください。 またDbContextPoolまでは踏み込めていません。後日調べようと思っています。

DbContext継承クラスは、基本的にDbContextOptions<TDbContext>を受け取るコンストラクタが必要です。

public partial class MyDbContext : DbContext
{
    public MyDbContext (DbContextOptions<MyDbContext > options) : base(options)
    {
    }
}

これをDIに登録するときはStartup.csに以下のようなコードを書きます。例はPomelo.EntityFrameworkCoreのものですが、SqlServerでも似たようなものです。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDbContext>(b => b.UseMySql(
            Configuration.GetConnectionString("ConnectionString"),
            new MySqlServerVersion(new Version(8, 0))
        )
    );
}

ラムダの引数であるところのbDbContextOptionsBuilder<MyDbContext>になります。DbContextOptions<TDbContext>を直接弄るのではなく、Builderのほうを弄ることで、弄った内容がOptionsに反映される、という形になっています。

AddDbContext<TDbContext>拡張メソッドで登録されるDI設定は、デフォルトがServiceLifetime.Scopedです。つまりWebの場合は同一リクエスト内ならば同じオブジェクトが使いまわされます。AddDbContext<TDbContext>オーバーロードによってはServiceLifetime.Scopedを引数で指定できますが、ほとんどのケースでScopedのほうが望ましいでしょう。

また、DbContext以外にもDbContextOptionsのほうもDI登録されていて、同じようにServiceLifetime.Scopedの寿命を持ちます。MyDbContextのコンストラクタに入ってくるDbContextOptionsはDIが解決しているのです。

思うにDbContextOptionsBuilder<MyDbContext>DbContextOptions<MyDbContext>ジェネリックな型になっているのはDIの都合です。もしジェネリックでないDbContextOptionsを引数に取るとしたら、DbContext継承型が複数あった場合、つまりMyDbContext1とMyDbContext2で引数が同じオブジェクトになってしまいますから、分離するために型をつけているのです。このあたりはちょっとしょぼいというか強引というか。

さて、ServiceLifetime.ScopedなのでMyDbContextはリクエスト毎にnewされるわけですが、コンストラクタ引数のDbContextOptionsも同じ寿命であって、リクエスト毎に以下のようなコードが呼び出されることになります。(簡略化してあります)

private DbContextOptions<TDbContext> CreateOptions<TDbContext>(Action<DbContextOptionsBuilder<TDbContext>>? optionAction)
{
    var builder = new DbContextOptionsBuilder<TContext>();
    optionAction?.Invoke(builder); //UseMySqlとかUseSqlServerとかがこの中で呼ばれてConnectionStringをセットしたりする
    return builder.Options;
}
//DI登録
services.TryAdd(new ServiceDescriptor(typeof(TDbContext), lifetime);
services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<TDbContext>), sp => CreateOptions(optionAction),  lifetime);

そもそもDbContextOptionsとは何ぞやというと、複数のIDbContextOptionsExtensionを束ねたDictionaryです。

abstract class DbContextOptions
{
    IReadOnlyDictionary<Type, IDbContextOptionsExtension> _extensions;
}

IDbContextOptionsExtension自体にめぼしい機能はありませんが、これを実装した抽象クラスであるRelationalOptionsExtensionがConnectionStringを持っており、SqlServerOptionsExtensionMySqlOptionsExtensionの親クラスになっています。

public abstract class RelationalOptionsExtension : IDbContextOptionsExtension
{
    string _connectionString;
    //他にもTimeoutとかMigrationAssemblyとか設定いろいろ
}

public class SqlServerOptionsExtension : RelationalOptionsExtension 
{
}

public class MySqlOptionsExtension : RelationalOptionsExtension 
{
}

DbContextOptionに対してUseMySqlMySqlOptionsExtensionを突っ込むし、UseSqlServerSqlServerOptionsExtensionを突っ込みます。

public static DbContextOptionsBuilder UseMySql(this DbContextOptionsBuilder builder, string connectionString,  ServerVersion serverVersion, Action<MySqlDbContextOptionsBuilder> optionsAction)
{
    MySqlOptionsExtension extension = GetOrCreateExtension(builder)
        .WithServerVersion(serverVersion)
        .WithConnectionString(connectionString);

    builder.AddOrUpdate(extension);

    optionsAction?.Invoke(new MySqlDbContextOptionsBuilder(builder));

    return builder;
}

さて、これでMyDbContextがnewされるまでの大体の要素が出そろいました。

  1. DbContextOptionsBuilderをnewする
  2. UseMySqlUseSqlServer相当のコードで、IDbContextOptionsExtensionDbContextOptionsBuilder.Optionsに登録する。この過程でConnectionStringが決まる
  3. ActivatorUtilities.CreateInstance<MyDbContext>DbContextOptionsBuilder.Optionsを引数に渡してインスタンスを作る
  4. 上記1~3を行うファクトリをDI登録する

これで生成周りを乗っ取れそうです。RDS自前シャーディングなどでConnectionStringをKeyによって切り替えたい時などに使えるかもと思っています。

C++でメタプログラミングの基礎

最近C++で学んだことのメモです。主にメタプログラミング周り。

twitter上ではRust人気に押されている感はありますがC++も進化を続けていて、特にconstexprに代表されるコンパイル時計算まわりは非常に強力だなと思えます。

C++98付近で知識が止まっているなら「へぇ、こんなことができるんだ」と思えるかもしれません。私は思いました。

内容はSFINAEメインです。本来はテンプレートの部分特殊化なども触れるべきだと思うのですが、使いこなしの経験が浅いので触れていません。

前提知識:型推論autoと戻り値型の後置

C#のvarに相当するものとしてautoが入りました。C#と違ってこれは戻り値にも使えます。

auto Hoge()
{
    return 1; //整数値リテラルの1から推論して戻り値はint
}

int main()
{
    auto x = Hoge(); //勿論変数にも使える
    std::cout << typeid(x).name() << '\n'; // int を出力。余談ですが最近 std::endl あまり見ないですね…
    return 0;
}

また、戻り値にautoを使いつつ後置で型を指定することができます。もちろん指定した型に変換できない値をreturnするとコンパイルエラーになります。

auto Hoge() -> int
{
    return 1;
}

前提知識:decltype

decltype()は式を受け取って、式の評価した結果の型を返します。

auto main() -> int
{
    int x { 10 };
    decltype(x) y = x + 10; //変数xを評価した結果はintなのでdectype(x)はint
    decltype(1 + 1) z = y; //1 + 1 を評価した結果はintなので decltype(1 + 1)はint

    std::cout << y << ' ' << typeid(decltype(y)).name() << '\n'; // 20 int
    return 0;
}

SFINAE

さて、templateと組み合わせてdecltypeを戻り値に使ってみます。

template<class T>
auto Hoge(T& t) -> decltype(t.Fuga(), true)
{
    std::cout << "TはFuga()を持つ" << '\n';
    return true;
}

auto Hoge(...) -> bool
{
    std::cout << "TはFuga()を持たない" << '\n';
    return false;
}

decltype(t.Fuga(), true)についてですが、C++のカンマ演算子は、カンマの前の式を評価しつつ結果は捨て、カンマの後ろの式を評価した結果を返します。よって、decltype(t.Fuga(), true)は、t.Fuga()を評価しつつ結果はtrueを評価したboolになりますのでHogeの戻り値はboolです。

しかし型引数T型のインスタンスtがFuga()を持たなかった場合、decltypeは失敗します。

ここでC++SFINAEというルールがでてきます。「Substitution Failure Is Not An Error (置換の失敗はエラーではない)」の略で、テンプレート解決に失敗した場合は、即座にコンパイルエラーにせずオーバーロードの解決から外れるだけという規則です。※もちろんオーバーロードが一つも見つからなかったらコンパイルエラーですが。

上の例では、Fuga()をもたない引数を渡したら Hoge(...) のほうに解決されるので、メソッドの有無で処理を静的に分岐できます。

struct X
{
    void Fuga() { }
};

struct Y
{
};

auto main() -> int
{
    X x{};
    Y y{};
    std::cout << Hoge(x) << '\n';
    std::cout << Hoge(y) << '\n';
}
TはFuga()を持つ
1
TはFuga()を持たない
0

補足:std::declval()

先の例のdecltype(t.Fuga(), true)についてですが、戻り値の型やtemplateの型引数にdecltypeを使う場合、式に用いる変数はナントカ型の変数であれば実体は何だって良いです。可能ならばdecltype(true)の代わりにdecltype(bool)とか書きたいくらいです。(書けませんが)

さて、「ナントカ型の変数であれば実体は何だって良い」モノを作るために、<utility>std::declval<T>()があります。

これを使うと、decltype(t.Fuga(), std::declval<bool>())と書くことができ、trueやfalseはどうだってよくただboolを返したいということが伝わりやすくなりますし、自作のstructを渡すときもコンストラクタがどうだっけ、などは気にしなくて良くなります。

detection イディオムとconcept

先の例ではオーバーロードで呼び出すメソッドを分岐しましたが、型が特定の条件を満たすかをboolの定数式でもらえるとif constexprコンパイル時分岐に使えたりほかにも面白い用途に使えたりします。型が特定の条件を満たすかを定数式で返すための最も簡単な方法はC++20で追加されたconceptだと思います。

template<class T>
concept HasFuga = requires(T & t) {
    t.Fuga();
};

conceptは、templateの型引数につけて型制約に使うことが多いと思いますが、それ自体がboolを返す定数式でもあります。

//
// 型制約としての用法
//

template<HasFuga T> //型制約 TはHasFugaでなければならない
struct C
{
};

struct X
{
    void Fuga() { }
};

struct Y
{
};

auto main() -> int
{
    C<X> x;
    C<Y> y; //コンパイルエラー! E3244 テンプレート制約がみたされていません
}
//
// bool型の定数式としての用法
//
auto main() -> int
{
    //constexpr なので、XがHasFugaを満たさない場合コンパイル時にこの分岐は消滅して実行時コストゼロ
    if constexpr (HasFuga<X>) {
        std::cout << "XはFugaを満たす" << '\n';
    }
    return 0;
}

std::enable_if:特定の条件を満たすときだけ関数を定義する

bool型の定数式をもらえるとできる面白いことの一つに、std::enable_ifがあります。これは<type_traits>ヘッダーに存在し、特定の条件を満たすときだけ関数を定義することができます。上手く使えばバイナリサイズを削減できそうです。

// std::enable_if<bool Condition, T>
// std::enable_if<bool Condition, T>::type は Tを返すので、
// std::enable_if<bool Condition, T>::type を戻り値としておけば
// ConditionがtrueのときはTが戻り値となるが、
// ConditionがfalseのときはSFINAEでオーバーロード解決から外される=関数が定義されない

template<class T>
auto DoFuga(T& t) -> std::enable_if<HasFuga<T>, void>::type
{
    t.Fuga();
}

detection イディオム

concept以前のC++で型が特定の条件を満たすかを検出してboolの定数式で返す手法はdetectionイディオムと呼ばれ以下の記事に詳しいです。 onihusube.hatenablog.com

conceptが使えるようになったのでdetectionイディオムはもう不要かというとそういうこともなく、conceptはfriendと組み合わせることができないようで非publicな関数などを検出できないのでまだdetectionイディオムは使いでがあります。