koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

clusterに入り浸るようになって半年経過したことについてのポエム

この記事はclusterユーザーと加速する非公式 Advent Calendar 2020の14日目の記事です.

clusterに来るようになったきっかけや,この半年の思い出についてつらつらと書きます.

clusterに入り浸るようになったきっかけ

僕がclusterで本格的に遊ぶようになったのは今年の6月末からです. きっかけは草羽エルさんというVTuberの方がきっかけになります.

Vket4の最終日に草羽エルさんがやっていたワールドツアーに偶然遭遇し,そこで草羽エルさんを知ることができました. そのときにTwitterをフォローしており,clusterでワールドを作り,イベントを開催するというのを知り,覗きにいってそこからclusterにハマりました.

僕は見知らぬ人と話をするのは得意ではなかったのですが,草羽エルさんがBar Angel Kiss,通称「えんきす」で開くイベントに毎回参加する常連の方々とは毎度顔を合わせるので,徐々に(急速に?)仲良くなることができました.

バーチャルなSNSは最初,知り合いがゼロの状態から始まるのですが,僕のようなコミュ障にとっては,ゼロの状態から仲の良い人を作るのは難しいわけです. えんきすがその障壁を突破するきっかけとなり,半年前では信じられないほど仲の良い人達ができました.

そして,毎日のようにclusterに入り,えんきすのイベントに参加したり,深夜遅くまで仲の良い人たち同士で何のこともない雑談を繰り広げるという生活が始まり,良い意味で僕の生活が変わりました(バーチャルな生活が始まりました).

おかげさまで,せっかく購入したはいいものの,割と放置気味になっていたHMDをこの半年で元を取る以上に使うことができたと思います.

えんきすの常連さんの中には,僕と同じようにえんきすがきっかけでバーチャルな生活が始まったという方が数多くいらっしゃるのではないでしょうか?

半年の思い出

今までにclusterで撮った写真のうち,ほんの一部を貼りまくります.

f:id:koturn:20201214030705p:plain f:id:koturn:20201214030710p:plain f:id:koturn:20201214030732p:plain f:id:koturn:20201214030737p:plain f:id:koturn:20201214030749p:plain f:id:koturn:20201214030756p:plain f:id:koturn:20201214030801p:plain f:id:koturn:20201214030809p:plain f:id:koturn:20201214030813p:plain f:id:koturn:20201214030816p:plain f:id:koturn:20201214030822p:plain f:id:koturn:20201214030828p:plain f:id:koturn:20201214030830p:plain f:id:koturn:20201214030834p:plain f:id:koturn:20201214030841p:plain f:id:koturn:20201214030845p:plain

写真を撮るということ

僕はかなり写真を撮影する人間なのですが(VRChatのも含めて25000枚程度!の写真がローカルにあります),それは単純に自分のために思い出に残したいという考えの他に,他の人のためにも写真を残しておきたい,という思いがあったりします.

案外自分を含めて写真を撮るのはちょっと手間がかかりますからね. 後でこういう写真撮れたよ~というのを共有したいわけです.

写真っていいもので,先程のように過去の写真を並べるだけで何かエモくなるものです.

そういうわけでみなさんも写真を撮りまくりましょう. いつか将来に見返したときにめっちゃエモい気持ちになりますよ!

おわりに

今の楽しいバーチャルの生活があるのはひとえにエルルナのお二人,clusterという場,そして仲良くしてくれている皆さんのおかげです. 本当にありがとうございます!

このコロナ禍でリアルで友人と会うことさえ憚られる世間の状況の中,ほぼ毎日の頻度で会う友人が出来たことは僕の人生においてかけがえのない経験・宝物であると思います. (コロナ禍以前のでリアルの友人と会う頻度よりも多いですね,バーチャルの手軽さって素晴らしい)

そして...本日は草羽エルさんの2周年記念日です! 数日前からclusterのイベントページにあるように,草羽エルさんをお祝いするイベントが開催されます. ぜひともお祝いにいきましょう!

おまけ

別のアドベントカレンダーの記事でPNGファイルの無劣化高圧縮についての記事を書きました.

ZopfliというGoogle発のアルゴリズム・ソフトウェアを利用すると,おおよそ20~25%程PNGファイルの容量を削減できることが確認できた,Zopfliいいね~,便利に使うためのラッパーツール書いたよ~って感じのことを書いています.

私のような写真を大量に撮る人間にとっては,20~25%の容量削減はけっこう重要になります. ローカルに写真を大量に保存している人は一度Zopfliで圧縮することを検討してみてもいいかもしれませんね.

また,テクスチャファイルもZopfliで圧縮すれば,VRMのファイルサイズをそれなりに減らすことが出来ることも確認出来たので(自分のアバターのもので25%程度),25MBのファイルサイズ制限に引っかかる人は検討してもいいかもしれません(そんな人いないと思いますがw).

ZopfliPNGによるPNGの可逆再圧縮とVRMの容量削減について

この記事はIQ1 Advent Calendar 2020の7日目の記事です.

TL;DR

zopflipng.dllをいい感じに使うツールを作りました.

  • 並列実行可能
  • zipアーカイブファイル内のファイルを一時的に書き出すことなく処理可能
  • 更新日時タイムスタンプはそのままで再縮率実施

また,最新のバイナリはここに置いてあります.

背景

Zopfli(ツオップリ)とはGoogleが開発したDeflate互換の圧縮アルゴリズムで下記の特徴があります.

  • Deflate互換で高い圧縮率
  • 当然,可逆圧縮
  • 圧縮にはかなりの時間が必要(100倍ぐらいの時間)

そもそも,Deflate圧縮とはzipやgzip形式のファイルに使用されている圧縮形式であり,zopfliを使用すれば単純に高圧縮率のzipファイルやgzipファイルを作成することができるわけですね. つまり,既存のツール(unzipやgunzipなど)でも圧縮結果を伸長することができるのがセールスポイントですね.

そして,PNGファイルに使用されている圧縮方式もDeflateであるため,その部分にもZopfliを適用することができるわけです.

PNGの保存容量を削減するなら,そのPNGファイルのディレクトリを7zip等でLZMA2とかの圧縮アルゴリズムを用いればよくない?と思うかもしれませんが,PNG自体がDeflate圧縮を用いているため,PNGファイル自体に対して何かしらの圧縮アルゴリズムを用いたとしてもほとんど容量の削減が出来ないわけです. なので,PNGファイルの中のDeflate圧縮部分を改善することには大きな意味があります.

GoogleによるZopfliのリファレンス実装のリポジトリでは,gzip形式の圧縮を行なうためのzopfliコマンドおよびPNGの再圧縮を行なうためのzopflipngをビルドすることが可能です.

コマンドだけではなく,.soや.dllといったライブラリファイルとしてビルドし,他の言語から呼び出すことも可能です.

Googleのリファレンス実装のZopfliPNGをデフォルトのオプション指定で利用した場合,20~25%程度の容量削減になることが確認できました. これだけの容量を削減しながら,全く画像の劣化が伴わないのは驚愕ですね.

ただし,1920x1080(FullHDの解像度)のPNG画像だと1枚あたりの処理時間が60~120秒といった具合で,大量の画像を処理するにはなかなかしんどいなといった具合です.

開発動機

何となくZopfliを使用したツールを作ろ~と思ってたら,できあがっていきました(簡単なツールを書いてプログラミング欲を満たしたかっただけ). バイナリはGoogleドライブに置いています.

が必要なはずなので,ご使用の際は上記2つがインストールされていることを確認し,インストールされていなければお使いの環境に合わせたものをインストールしてください.

後付けの動機としては,今年の5月ぐらいからVRChatやclusterといったVR・バーチャルSNSにハマり,写真撮影をしまくるために,PNG画像が大量に貯まってきたので,それをいい感じに圧縮したかったというところです. (ローカルに20000枚ぐらいのPNGファイルがあります...!!)

clusterの写真は撮影後にcluster側に保存され,Webからダウンロードできるのですが,まとめてダウンロードする場合はzipファイルにまとめられています. clusterのPNG画像ファイルはGUID名になっており,タイムスタンプのみが保存日時を知る術となっているので,再圧縮するにしてもタイムスタンプをそのままにしたかったというのと,いちいちディスクに一時ファイルを書き出して再度zipファイルに格納するのは明らかに冗長だと思いました.

また,前述の通り,処理にはものすごく時間がかかるので,1枚の画像の処理に対して1スレッドを割り当てて並列処理を行えば,多少はマシになるかなと思った次第です.

普通はGoogleのリファレンス実装のzopflipngコマンドをそのままシェルスクリプトやバッチファイル等で回せばいいと思うものですが,上記の要件

  • 並列実行可能
  • zipアーカイブファイル内のファイルを一時的に書き出すことなく処理可能
  • 更新日時タイムスタンプはそのままで再縮率実施

を満たすのはしんどいので,とりあえずC# で書きました.

ツールの使用方法

残念ながら(?),私はGUIのツールがあまり好きではないので(自動化しにくいのと自分にとって不要であるのと作るのがめんどくさいので),現状はコマンドラインツールとして開発しています.

基本的に対象がzipファイルの場合,

> ReCompressPng.exe [対象のzipファイル]

ディレクトリが対象の場合,

> ReCompressPng.exe [対象のディレクトリ]

と指定すればOKです. 同時実行スレッド数はデフォルトでは無制限なので,他の作業を並行して行なうために制限したい場合は -n オプションで指定することが可能です. 下記は同時実行スレッド数を2に制限する例です.

> ReCompressPng.exe -n 2 [対象のzipファイル]

オプション表

オプション オプション引数 説明
-c, --count-only 対象zip・ディレクトリ内のPNGファイルとそのサイズ,および合計サイズを表示します.
-h, --help ツールの使用方法を表示し,プログラムを終了します.
-i, --num-iteration 繰り返し回数 Zopfliの繰り返し回数を指定します.
-I, --num-iteration-large 繰り返し回数 大きな画像データに対するZopfliの繰り返し回数を指定します.
大きな画像の閾値は身長後の画像データ長が200000 Bytes以上かどうかです.
-n, --num-thread スレッド数 同時実行スレッド数を指定します.
-r, --replace-force ZopfliPNGによる再圧縮後のPNGファイルのサイズがオリジナルのPNGのサイズより大きくても置きかえを行ないます.
-s, --strategies カンマ区切りの数字 Zopfliのストラテジをカンマ区切りで指定します.例:-s 0,1,2,3,4,6,7 .詳細はZopfliPNGのソースコード等を参照してください.
-v, --verbose カンマ区切りのチャンク名 zopflipng.dllからの標準出力を有効にします.
--keep-chunks 指定したチャンクは除去しないようにします.
--lossy-transparent (※非可逆圧縮)アルファ値0の画素のRGB設定値を0にします.
--lossy-8bit (※非可逆圧縮)16bit画像を8bit画像にします.
--no-overwrite 対象ファイルの上書きを行なわず,別のファイルとして再圧縮結果のPNGファイルを出力します.
--no-auto-filter-strategy ストラテジの自動選択を行なわないようにします.
--no-use-zopfli Zopfliを利用しないようにします.
--no-verify-image 圧縮前後のPNG画像データの比較を行なわないようにします.

注意点

確かにZopfliPNGによる再圧縮では高圧縮率のPNGファイルを得ることができました. しかし,他のペイントツール等で読み込み,そのツールで保存を行なった場合には,通常のPNGのDeflate圧縮が行なわれてしまうため,ファイルサイズが元に戻ってしまいます(可逆圧縮なので当然ですが). ですので,この場合は再度ZopfliPNGに通さなければPNGファイルのサイズが元に戻ったままとなります.

興味本位の調査

VRSNSで使用するアバターは当然テクスチャ画像が含まれているわけですが,このテクスチャ画像のうち,PNGのものをZopfliPNGで再圧縮するとアバターが軽量化できるのではないかと思って,やってみました. アバターの軽量化はネットワーク負荷を軽減させることになるため,他の人のためにもできる限りアバター容量を小さくしたいという思いがあります. (特にclusterでは)

VRM

普段僕がclusterで使用しているアバターは,VRoid Studioで作成し,VRMConverterでUnityに取り込み,UniVRMで出力したものです.

f:id:koturn:20201207193236p:plain

これに対し,UnityプロジェクトのAssetsディレクトリ内のPNGファイル全てをZopfliPNGで圧縮し,全く同じ出力設定で比較を行いました.

結果としては...

f:id:koturn:20201207193541p:plain

なんと,約9MBから6.8MBに容量が削減できました!

VRM形式は全然詳しくはありませんが,結果を見る限りではおそらく,テクスチャ等のPNGファイルをそのまま格納しているのだろうと予想します.

ただ,VRMファイルが軽くなったからといって,ネットワーク負荷が軽くなるかどうかはわからないところです. 例えば,バーチャルSNS:clusterでサーバ側にあるVRMファイルをそのままクライアントに送りつけて,クライアントで展開するという実装になっているのであれば,VRMファイル自体の容量が小さいことに意味はあります. しかし,サーバ側でVRMをクライアントにとって扱いやすい形式に変換し,送りつけているのであれば,画像は一度伸長されていると思われるので,ZopfliPNGによる圧縮はネットワーク負荷の軽減には全く寄与しないわけですね.

とはいえ,VRM自体のファイル容量は削減できているので,アップロードの25MB制限にひっかかったときにZopfliPNGが使えるかもしれないですね(アバターに依りますが25%の容量削減が出来たのはかなり大きいかと!). まぁ,ポリゴン数とかボーン数に引っかかる人がほとんどで,25MB制限に引っかかる人は稀な気がしますが....

VRChat

実験にはBoothで販売されているアバター響狐リクちゃんを使用しました. とても可愛いですね.

これもUnityプロジェクトのAssetsディレクトリ内のPNGファイル全てをZopfliPNGで圧縮してみました. その結果は...

f:id:koturn:20201207192640p:plain f:id:koturn:20201207192644p:plain

なんと容量に変化はありませんでした! 一応,テストビルドのVRChatのアバターファイルを確認したところ,1KB程度の容量の削減は確認できましたが,雀の涙程度でしかないですね....

おそらく画像ファイルは一度展開し,まとめるとか,独自の形式にしているとかやっているのではないかと思われます.

今後の展望

GUIを付ける

一般人ウケを考えると必須な作業ですね. しかしやる気がないので,多分やらないです.

VRMファイル内のPNG画像の再圧縮

VRMファイルの形式には詳しくないですが,PNGの再圧縮によりファイルサイズを減少させることができたので,おそらくPNGファイルはそのままVRMファイル内に格納されているのだと予想できます. となれば,VRMファイル内のPNGファイルを再圧縮し,再度VRMファイルを出力するツールを作ってみたくなりますね.

keep_colortype

Googleの提供しているZopfliPNGのC APIだと,keep_colortype を指定する術がありません

keep_colortype が指定できない場合,現在の実装では keep_colortype = false となるため,例えば,32bit ARGBのPNG画像かつアルファ値が全画素で255(透過しない)となっていると24bit RGB形式に変換してしまうわけです(無駄なアルファチャンネルの削除をするわけですね). この操作では当然視覚的な変化は全く無いし,再度全画素にアルファ値255を加えれば元に戻せるわけですが,厳密には可逆ではない圧縮ということになりますね.

keep_colortype をC++ APIの方からは指定する術はあるのですが,C# 側からこの関数の呼び出しを行うのは,マングルされた関数名を指定してやる必要があるため,しんどいわけです.

となれば,別のDLLにC++ APIを呼び出すだけのC APIの関数を用意するのが,手元で可能なワークアラウンドとなりますが,そのためにDLL 1個を増やすのはなぁ...といったところです.

keep_colortype が追加されたのは 2020-05-24であるため,単にC APIの方への追加忘れではないかと思います. これはプルリクチャンスですかね...誰かやってもらえると助かります.

(配布しているバイナリではzopfli本体のソースコードを修正し,zopflipng.dllを再ビルドした上で,RecompressPng自体も対応するようにしているので,--keep-color-type というオプションを使うことができます.)

余談

ちなみに,zopfliのリポジトリには,gccを使ったビルド方法が簡単に書かれているだけで,MSVCでのビルド方法が書かれていないですが,CMakeLists.txt があるので,それを利用してビルドを行なうのが簡単です.

ただし,普通にビルドしたのでは単純にコマンドラインツールのみが生成されるので,DLLを生成するために, ZOPFLI_BUILD_SHAREDON にする必要があります.

下記はビルド手順の一例です. 最初に,zopfliのリポジトリのトップディレクトリにいるものとします.

> mkdir build
> cd build
> cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DZOPFLI_BUILD_SHARED=ON
> cmake --build .

あくまでリファレンス実装であるため,ソースコードがドキュメントだ!と言わんばかりの姿勢がいいですね.

まとめ

とにかくZopfli/ZopfliPNGはすごいよということが言いたかっただけですね. 副産物としてVRMのファイルサイズ削減できることを発見できたのは大きな収穫でした.

みんなもZopfliPNGで色々なPNG画像を圧縮しましょう!

僕の配布しているバイナリが怪しいという意識をお持ちの方は,逆コンパイラ等で中身見て安全であることを確認するなり,GitHubソースコードを落としてきてビルドするなり,自前でプログラム書くなり,オリジナルのZopfliを使うなりするとよいと思います.

一応,Visual Studioやcmakeを用意するのが面倒な人のために,オリジナルのZopfliをビルドしたものも置いてあります

参考

Unityでの変数・プロパティのアクセス性について考える

前置き

最近,Unityに触れており,C#を書くことが多い. 古いUnityの文化はわからないが,publicメンバ変数が気持ち悪くて仕方がない. (おそらく,古代のUnityではプロパティを使用できない,あるいは実行処理系がプロパティのインライン展開を行うことができなかったのではないかと思うが.... また,インスペクタ表示の兼ね合いもあったのかもしれない.)

普通のC#を書いてきた身としては,publicメンバ変数は用いず,代わりに(自動実装)プロパティを用いるべきであるという思いがある. なので,超初歩的な内容ではあるが,メンバ変数宣言とプロパティについてまとめることにした.

変数とプロパティのアクセス表

基本的に使用することがあるのは下記の表のものだと思う. T は適当な型を指す.

プロパティについては自動実装プロパティのみを記載している (public 変数 との対比であるので).

もし,private変数とプロパティを別々に記述する場合は,インスペクタ表示は変数の欄,アクセス性はプロパティの欄に従うことになる.

protectedinternal のような他のアクセス修飾子は些細な違いしかないので省略している. また,setだけのプロパティは普通作らないので,これも省略した.

No. 宣言 自クラスでの値取得 自クラスでの値設定 他クラスから値取得 他クラスから値設定 インスペクタ表示
1 public T foo;
2 public readonly T foo; △(※1) △(※2)
3 private T _foo;
4 private readonly T _foo; △(※1)
5 [SerializeField]
private _foo;
6 public T Foo { get; set; }
7 [field: SerializeField]
public T Foo { get; set; }
8 public T Foo { get; private set; }
9 [field: SerializeField]
public T Foo { get; private set; }
10 public T Foo { get; } △(※1)
11 [field: SerializeField]
public T Foo { get; }
△(※1)

No.1, No.2はpulbicメンバ変数なので使用すべきでない,No.11のgetオンリーなプロパティはたとえ SerializeField 属性を付与しようとも,バッキングフィールドが readonly であるため,インスペクタからは見えず,SerializeField の意味がないという点で使用すべきでない. なので,No.3~No.10の8つのうち,いずれかを使用すべきである.

※1:初期化時のみ値設定が可能. 例えば,コンストラクタ内や,

public class Foo
{
    private readonly int _hoge;

    public int Fuga { get; }

    Foo(int hoge, int fuga)
    {
        // これはOK
        _hoge = hoge;
        Fuga = fuga
    }

    // SetAndShow(int hoge, int fuga)
    // {
    //     // これはNG
    //     _hoge = hoge;
    //     Fuga = fuga;
    // }
}

宣言と同時に初期化では可能.

public class Foo
{
    private readonly int _hoge = 42;

    public int Fuga { get; } = 84;
}

MonoBehaviour 継承クラスにコンストラクタを実装することはないので,Unityではあまりreadonlyなメンバ変数とgetオンリーな自動実装プロパティを利用する機会は少ないかもしれない.

※2: 値型はunsafeな手段を用いれば書き換え可能.

class Program
{
    static void Main(string[] args)
    {
        var f = new Foo();
        Console.WriteLine(f.hoge);  // => 42
        unsafe
        {
            fixed (int* p = &f.hoge)
            {
                *p = 20;
            }
        }
        Console.WriteLine(f.hoge);  // => 20
    }
}

public class Foo
{
    public readonly int hoge = 42;
}

自動実装プロパティのインスペクタでの表示

自動実装プロパティの場合,バッキングフィールド名がそのままインスペクタに表示されてしまう. それが嫌なら,

ここにある RenameFieldAttribute のようなものを用いて,インスペクタの表示を操作してやるか,

[field: SerializeField]
[field: RenameField("Foo")]
public int Foo { get; set; }

自動実装プロパティをやめ,実態となる [SerializeField] private変数を別に用意し,それに対するプロパティを用意する必要がある.

/// <summary>
/// インスペクタからは見えるが,他クラスからは見えない
/// </summary>
[SerializeField]
private int _foo;

/// <summary>
/// 他クラスから<see cref="_foo"/>を見るためのプロパティ
/// </summary>
public int Foo
{
    get { return _foo; }
    set { _foo = value; }
}

UdonSharpについて

なお,現在のUdonSharpでは自作クラスにプロパティを使用することはできないし,そもそもpublicメンバ変数であっても普通にアクセスすることもできない. なので,おそらく裏でリフレクションを使用しているであろう手段でアクセスすることになるが,これはアクセス修飾子を無視してアクセスできるので,おとなしくpublicメンバ変数を用いるのがよいと思われる(そのうち普通にアクセスできるようになると思うし,変数publicだと他クラスでも nameof() で変数名を取得できるため,typoを実行時ではなくコンパイル時に検知できる).

まとめ

基本的にはprivate変数かpublicプロパティのどちらかの選択となり,あとはインスペクタ表示が必要かどうかで判断することになる.

  1. 外部クラスから見える必要があるか?
    • Yes: プロパティ
    • No: private変数
  2. インスペクタから操作できる必要があるか?(Unityに限る)
    • Yes: [SerializeField] を付ける
    • No: そのまま
  3. 値設定は初期化時のみに限定したい?
    • Yes: readonlyな変数 / getオンリーなプロパティ
    • No: そのまま

参考

C#のシングルトン

背景

最近,UnityでC#を書くので,普通のC#を書くときに使っていたコードスニペットを整理していた. その中にシングルトンの実装が含まれているのを見て,そういえばこの実装はスレッドセーフなのか?と疑問に思ったのがきっかけで,C#のシングルトンについて調べ直した. この記事では備忘録としてシングルトンの各種実装をまとめた.

4つのシングルトン実装

実装その1(非スレッドセーフ)

単純な実装は下記の通り.

初回に Instance プロパティを通じてインスタンスの取得を試みたとき,インスタンスの作成と保持を行い,保持したインスタンスを返す. 2回目以降は保持したインスタンスを返す. いわゆる遅延初期化という実装である.

当然だがスレッドセーフではない. また,些細なものだが,アクセスの度にnullチェックが入るのが気になる.

public sealed class Singleton
{
    private static Singleton _instance;

    public static Singleton Instance => _instance ??= new Singleton();

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その2(スレッドセーフっぽいけど保証はされないらしい)

なればとクラス自体の初期化が行われた際にインスタンスを作成する下記の実装を考える.

一見スレッドセーフっぽいが,実はスレッドセーフであることは保証されないとのこと.

静的コンストラクタが定義されていないクラスには,beforefieldinit属性が付加される. そして,beforefieldinit属性が付与されていると,

  1. 確実に一回だけ、type initializer が呼ばれる
  2. type initializer が完了するまで、他のスレッドが静的フィールドにアクセスしたとしても待たされる

という事項がCLI仕様として保証されないらしい

また,初期化タイミングがその型の静的フィールドへの初回アクセス時になるとは限らず,それ以前になることもあるかもしれないらしい.

public sealed class Singleton
{
    public static Singleton Instance { get; } = new Singleton();

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その3(スレッドセーフ(コンパイラ依存らしいが))

というわけで,静的コンストラクタを定義すればよい. 中身は空でもよい.

実際,その2の生成コードとその3の生成コードを比較すると,前者にはbeforefieldinit属性が付与されており,後者には付与されていないことが確認できた.

ただし,これはコンパイラ依存であるらしく,コンパイラ非依存のシングルトンにするためには,別の手法が必要になる.

個人的にはこの実装でよいと思うが....

public sealed class Singleton
{
    public static Singleton Instance { get; } = new Singleton();

    static Sigleton()
    {
        // beforefieldinit属性を付与されないための静的コンストラクタ
        // 空だが消去してはならない
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

下記のような静的コンストラクタで初期値代入を行うようにしてもよいと思う.

public sealed class Singleton
{
    public static Singleton Instance { get; }

    static Sigleton()
    {
        Instance = new Singleton();
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その4(スレッドセーフ(コンパイラ非依存))

最初の遅延初期化の案にロックを使用するようにすれば,確実にスレッドセーフなシングルトン実装になる. いわゆる,Double Checked Lockingである.

ロックはそのスレッドから見て,シングルトンインスタンスが生成されていない場合にのみ行うので,アクセスの度にロック取得を行なうわけではない.

ただ,getプロパティのバイトコードサイズが手元のコンパイル結果だと83 Bytesになっており,lockも含むので,実行時にインライン展開されるかどうかはかなりあやしい(未確認).

public sealed class Singleton
{
    private static Singleton _instance;

    private static readonly object _syncRoot = new object();

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_syncRoot)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

ジェネリックなシングルトン実装

複数のシングルトンクラスの実装を行うと,似たようなコードを書いて実装という形になる. ここを何とかしたいと人間考えるものである.

なので,ジェネリックなシングルトンクラスを考える. ベースとしては前述の実装その3を採用する.

先に結論を書くと,一長一短であり,理想的な実装はない. 個別にシングルトンクラスを定義するのが一番よい.

実装その1(コンストラクタをpublicにせざるを得ない)

C++でいうところのCRTPに似た形でジェネリックなシングルトンクラスの実装を行う. C++のフレンドクラスのような機能はC#にはないため,シングルトンにするクラスのコンストラクタを public にしなければ,ジェネリッククラス側でコンストラクタが不可視のため,コンパイルできない.

public abstract class Singleton<T>
    where T : class, new()
{
    public static T Instance { get; }

    static Singleton()
    {
        Instance = new T();
    }
}

public sealed class Foo : Singleton<Foo>
{
    public string Name { get; }

    public Foo()
    {
        Name = nameof(Foo);
    }
}

実装その2(リフレクションを利用)

リフレクションを利用すれば,privateなコンストラクタへアクセスできる.

しかし,

  1. インスタンスの生成のコストが高い.
  2. 引数有りコンストラクタを定義すると,デフォルトコンストラクタが定義されなくなり,実行時エラーになる.コンパイルエラーにはできない.

という問題がある.

public abstract class Singleton<T>
    where T : class
{
    public static T Instance { get; }

    static Singleton()
    {
        Instance = (T)Activator.CreateInstance(typeof(T), true);
    }
}

public sealed class Foo : Singleton<Foo>
{
    public string Name { get; }

    private Foo()
    {
        Name = nameof(Foo);
    }
}

まとめ

様々なシングルトンクラスの実装を紹介した.

静的コンストラクタの有無によって,beforefieldinit属性の有無も変化し,それによってCLIの仕様として,スレッドセーフな初期化が保証されるかどうかが変わるため,遅延初期化でない実装では,空でも静的コンストラクタを定義した方がよい.

また,完璧なジェネリックシングルトンクラスの実装はできない.

参考

IQ1の人間が書いているVimの設定の一部

この記事はIQ1 Advent Calendar 2018の10日目の記事です.

特に書くことを決めてなかったのと,最近Vimconf2018やOsaka.vim等でVimに対するモチベーションが高まっているのでVimのことを書きたいと思います. Vimのモチベーションが高まっていなかったら,艦隊これくしょんの装備や編成について書いていたかもしれないです.

何を書くか

既に自分の .vimrc に書いてある設定のうちのいくつかを紹介していきたいと思います.

存在しないディレクトリのファイルを保存するときにディレクトリを作成する

例えば,hoge というディレクトリが存在しないときに,

$ vim hoge/fuga.txt

でファイル編集を開始したとき,ファイルを保存しようとしても hoge が存在しないため,ファイル保存ができません.

そこで以下のような autocmd を定義しておくことで,ファイル保存時にディレクトリを作成するかどうかを尋ねるようになります.

function! s:auto_mkdir(dir, force) abort " {{{
  if !isdirectory(a:dir) && (a:force || input(printf('"%s" does not exist. Create? [y/N]', a:dir)) =~? '^y\%[es]$')
    call mkdir(iconv(a:dir, &enc, &tenc), 'p')
  endif
endfunction " }}}
autocmd BufWritePre * call s:auto_mkdir(expand('<afile>:p:h'), v:cmdbang)

:w ではなく,:w! で保存した場合は,ディレクトリ作成するかどうかを尋ねることなく,ディレクトリを作成します.

新規作成ファイルにshebangがある場合,保存時に実行可能属性を与える

シェルスクリプトなどでファイルにshebangを記す場合,自動的に実行可能属性を付与する設定です.

if s:executable('chmod')
  function! s:add_permission_x() abort " {{{
    autocmd! Permission BufWritePost <buffer>
    if !stridx(getline(1), '#!')
      silent system('chmod u+x ' . shellescape(expand('%')))
    endif
  endfunction " }}}
  augroup Permission " {{{
    autocmd!
    autocmd BufNewFile * autocmd Permission BufWritePost <buffer>  call s:add_permission_x()
  augroup END " }}}
endif

Vim8のターミナル機能内でVimを起動したとき,親のVimでファイルを開くようにする

Vimのアドベントカレンダー(その2)の4日目の記事:terminal に関する小さい Tips - Qiitaにて,Vim8の :terminal にはTapiという機能があることを知りました.

タイトルにもあるやりたいこと=ターミナルから親のVimでファイルを開くことは既に上記の記事で行われているのですが,ファイルセレクタではなくデフォルトで存在しているであろうコマンドのみで,かつ渡した引数のファイルのみを開くようにしたいと考え,以下のような autocmd を定義してみました.

function! Tapi_Drop(bufnum, arglist) abort " {{{
  let [pwd, argv] = [a:arglist[0] . '/', a:arglist[1 :]]
  for arg in map(argv, 'pwd . v:val')
    execute 'drop ' . fnameescape(arg)
  endfor
endfunction " }}}

autocmd TerminalOpen *bash*,*zsh* call term_sendkeys(bufnr('%'), join([
      \ 'function vimterm_quote_args() { for a in "$@"; do echo ", \"$a\""; done; }',
      \ 'function vimterm_drop() { echo -e "\e]51;[\"call\", \"Tapi_Drop\", [\"$PWD\" `vimterm_quote_args "$@"`]]\x07"; }',
      \ 'alias vim=vimterm_drop'
      \], "\n") . "\n")

上記を .vimrc に記述した上でターミナルを起動すると,与えた引数をTapiのcallによってVim側に定義した関数に丸ごと引き渡すエイリアスを行い, vim というコマンドを置き換えます. エイリアスの本体は,引数をダブルクオートしてカンマ区切りにするシェル関数になっています. これらのエイリアスやシェル関数の定義は autocmdTerminalOpen のタイミングで term_sendkeys() を利用することで行います.

term_sendkeys() を利用するので,起動時にエコーバックがあることや,cmd.exeに非対応であるのが難点ですが,徐々に改善していきたいと思います(最近考えた設定なので).

使い方は簡単で, :terminalbashもしくはzshを起動し,そのシェル上で

$ vim hoge.txt

とするだけです. これで,親のVimでファイルを開くことができます.

他のディレクトリに移動したり,hoge/fuga.txt のようなファイルパスだったり,ho ge/fuga/txtのようなスペースを含むファイルパスでも問題はないようにしています(多分).

行末スペースを削除する

行末スペースを削除するコマンドです. jumplist や 検索履歴を汚すことなく,またカーソル位置を移動することなく置換するようにしてあります.

function! s:delete_match_pattern(pattern, line1, line2) abort " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/' . a:pattern . '//ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% DeleteTrailingWhitespace  call s:delete_match_pattern('\s\+$', <line1>, <line2>)

句読点をカンマ・ピリオドに置換する

句読点をカンマ・ピリオドに置換するコマンドです. ビジュアルモードで選択した行のみを置換することも可能になっています. 卒論,修論の執筆に必要になることもあるでしょう.

function! s:comma_period(line1, line2) abort range " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/、/,/ge'
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/。/./ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% CommaPeriod  call s:comma_period(<line1>, <line2>)

カンマ・ピリオドを句読点に置換する

反対にカンマ・ピリオドを句読点に置換するコマンドです. 入力でデフォルトでカンマ・ピリオドが入力されるように設定している場合,このコマンドが必要になることがあります.

function! s:kutouten(line1, line2) abort range " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/,/、/ge'
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/./。/ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% Kutouten  call s:kutouten(<line1>, <line2>)

行頭のみの retab コマンド

Vimには retab コマンドというコマンドがあります. これは,スペースとタブが混在している場合, expandtab の設定状況に応じて,スペースやタブに統一するコマンドです. ただし,行頭以外にも適用されてしまうため,それがあまり好ましくないと思える場面もあります.

そこで,行頭のみに適用できるように retab コマンドをエミュレーションするコマンドを定義しています.

function! s:retab_head(has_bang, width, line1, line2) abort " {{{
  if &l:tabstop != a:width
    let &l:tabstop = a:width
  endif
  let spaces = repeat(' ', a:width)
  let cursor = getcurpos()
  if &expandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), spaces, "\t", "g"), "\t", spaces, "g")/ge' :
          \ 's/^\(\s*\t\+ \+\|\s\+\t\+ *\)\ze[^ ]/\=substitute(submatch(0), "\t", spaces, "g")/ge')
  else
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), "\t", spaces, "g"), spaces, "\t", "g")/ge' :
          \ 's#^\(\s*\t\+ \+\|\s\+\t\+ *\)\ze[^ ]#\=repeat("\t", len(substitute(submatch(0), "\t", spaces, "g")) / a:width)#ge')
  endif
  call setpos('.', cursor)
endfunction " }}}
command! -bar -bang -range=% -nargs=?  RetabHead  call s:retab_head(<bang>0, add([<f-args>], &tabstop)[0], <line1>, <line2>)

range 指定もしてあるので,ビジュアルモードで選択した範囲のみにコマンドを適用することもできます.

インデントをスペースかタブか切り替える

これも前述のものと似たものですが,インデントにスペースを用いるかタブを用いるか切り替えます. 切り替えにあたって, 'expandtab' オプションの設定状況もトグルするようになっています.

単純なスペース・タブ置換とは異なり,行頭以外には適用されないためベンリです(多分).

function! s:toggle_tab_space(has_bang, width, line1, line2) abort " {{{
  let [&l:shiftwidth, &l:tabstop, &l:softtabstop] = [a:width, a:width, a:width]
  let [spaces, cursor] = [repeat(' ', a:width), getcurpos()]
  if &expandtab
    setlocal noexpandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), "\t", spaces, "g"), spaces, "\t", "g")/ge' :
          \ 's#^ \+#\=repeat("\t", len(submatch(0)) / a:width) . repeat(" ", len(submatch(0)) % a:width)#ge')
  else
    setlocal expandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(submatch(0), "\t", spaces, "g")/ge' :
          \ 's/^\t\+/\=repeat(" ", len(submatch(0)) * a:width)/ge')
  endif
  call setpos('.', cursor)
endfunction " }}}
command! -bar -bang -range=% ToggleTabSpace  call s:toggle_tab_space(<bang>0, &l:tabstop, <line1>, <line2>)

これもビジュアルモード指定している範囲のみに適用することができます.

手動補完のヒント表示

これは以前 VimのCtrl-X補完を使えるようになりたい - koturnの日記にも書いた内容です.

Vimにはプラグインに頼らなくても補完する機能があるのですが, <C-x> から始まる補完は12種類あるので覚えるのが大変です(特にIQ1には). なので,<C-x> を押下した時点でヒントを echo で表示するようにしてみました.

let s:compl_key_dict = {
      \ char2nr("\<C-l>"): "\<C-x>\<C-l>",
      \ char2nr("\<C-n>"): "\<C-x>\<C-n>",
      \ char2nr("\<C-p>"): "\<C-x>\<C-p>",
      \ char2nr("\<C-k>"): "\<C-x>\<C-k>",
      \ char2nr("\<C-t>"): "\<C-x>\<C-t>",
      \ char2nr("\<C-i>"): "\<C-x>\<C-i>",
      \ char2nr("\<C-]>"): "\<C-x>\<C-]>",
      \ char2nr("\<C-f>"): "\<C-x>\<C-f>",
      \ char2nr("\<C-d>"): "\<C-x>\<C-d>",
      \ char2nr("\<C-v>"): "\<C-x>\<C-v>",
      \ char2nr("\<C-u>"): "\<C-x>\<C-u>",
      \ char2nr("\<C-o>"): "\<C-x>\<C-o>",
      \ char2nr('s'): "\<C-x>s",
      \ char2nr("\<C-s>"): "\<C-x>s"
      \}
let s:hint_i_ctrl_x_msg = join([
      \ '<C-l>: While lines',
      \ '<C-n>: keywords in the current file',
      \ "<C-k>: keywords in 'dictionary'",
      \ "<C-t>: keywords in 'thesaurus'",
      \ '<C-i>: keywords in the current and included files',
      \ '<C-]>: tags',
      \ '<C-f>: file names',
      \ '<C-d>: definitions or macros',
      \ '<C-v>: Vim command-line',
      \ "<C-u>: User defined completion ('completefunc')",
      \ "<C-o>: omni completion ('omnifunc')",
      \ "s: Spelling suggestions ('spell')"
      \], "\n")
function! s:hint_i_ctrl_x() abort " {{{
  let more_old = &more
  set nomore
  echo s:hint_i_ctrl_x_msg
  let &more = more_old
  let c = getchar()
  return get(s:compl_key_dict, c, nr2char(c))
endfunction " }}}

inoremap <expr> <C-x>  <SID>hint_i_ctrl_x()

レジスタのヒント表示

これも VimのCtrl-X補完を使えるようになりたい - koturnの日記のおまけに書いた内容です.

IQ1にとっては,Vimレジスタに何が入っているかを覚えるのは困難です. そこで,レジスタを参照するキーを押下したときにヒント表示するようにしてみました.

function! s:hint_cmd_output(prefix, cmd) abort " {{{
  redir => str
    execute a:cmd
  redir END
  let more_old = &more
  set nomore
  echo str
  let &more = more_old
  return a:prefix . nr2char(getchar())
endfunction " }}}
nnoremap <expr> m  <SID>hint_cmd_output('m', 'marks')
nnoremap <expr> `  <SID>hint_cmd_output('`', 'marks') . 'zz'
nnoremap <expr> '  <SID>hint_cmd_output("'", 'marks') . 'zz'
nnoremap <expr> "  <SID>hint_cmd_output('"', 'registers')
if exists('*reg_recording')
  nnoremap <expr> q  reg_recording() ==# '' ? <SID>hint_cmd_output('q', 'registers') : 'q'
else
  nnoremap <expr> q  <SID>hint_cmd_output('q', 'registers')
endif
nnoremap <expr> @  <SID>hint_cmd_output('@', 'registers')

カーソル操作停止時のみカーソル位置をハイライトする

Vimには現在行をハイライトする 'cursorline' というオプションと現在列をハイライトする 'cursorcolumn' というオプションがあるのですが,これをカーソルの動きをとめた場合のみ有効にする設定です.

command! -bar ToggleCursorHighlight
      \   if !&cursorline || !&cursorcolumn || &colorcolumn ==# ''
      \ |   set   cursorline   cursorcolumn
      \ | else
      \ |   set nocursorline nocursorcolumn
      \ | endif
autocmd CursorHold,CursorHoldI,WinEnter *  set cursorline cursorcolumn
autocmd CursorMoved,CursorMovedI,WinLeave *  set nocursorline nocursorcolumn

最後に

この記事では僕が .vimrc に記述している設定を紹介しました. 割とありきたりなコマンドも紹介しましたが,カーソル位置をそのままにしておくことや,ビジュアルモードで選択した範囲のみに適用可能にしたり,検索履歴等を汚さないようにしているようにこだわってもいます. Vim力が上がるにつれて,少しずつ設定の挙動を改善できるようになるのもVimの面白いところですね.

参考文献

C++におけるfinallyの実装

はじめに

この記事は世間では十分に議論され尽くしてきたC++におけるfinally句という話について書こうと思う.

C++にはfinally無くて不便ですよね」という言葉は,実際にお仕事をしていて聞くことのある言葉なのだが,やはりC++初心者はfinallyが無い理由を考えないものであるらしい. C++にはRAII(Resource Aquisition Is Initialization)という考え方があり,これはリソース獲得と破棄をctorとdtorを使ってうまくやるというものである.

JavaC#といった言語はGCがあり,ファイナライザやdtorの呼び出しが制御できないようになっている. そのため,わざわざfinally句という枠組みが必要になってしまう. また,同様の理由でtry-with-resource文やusing文といった構文も必要になってしまうわけだ.

しかし,C++のdtorはローカル変数に限れば,その変数の寿命が尽きるとき,すなわちスコープを抜けるときに実行される仕組みとなっている. これがC++がfinally句を必要としない理由である.

ただし,あらゆるリソースの獲得と破棄のコードのためのクラスをいちいち用意していたのでは面倒である. そこで,C++11で取り入れられたラムダ式を利用することにより,より柔軟なRAIIクラスの設計が可能となる.

C++におけるfinallyの実装

以下のコードがfinally句を実現するためのクラスである.

#include <new>
#include <type_traits>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
Finally final
{
private:
#if __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L
  static_assert(std::is_invocable_v<F>, "[Finally] The finally function must be callable with no arguments.");
#else
  struct is_invocable_impl
  {
    template <
      typename T,
      typename... Args
    >
    static auto
    check(T&& obj, Args&&... args) -> decltype(obj(args...), std::true_type{});

    template <typename...>
    static auto
    check(...) -> std::false_type;
  };  // struct is_invocable_impl

  template <
    typename T,
    typename... Args
  >
  struct is_invocable :
    public decltype(is_invocable_impl::check(std::declval<T>(), std::declval<Args>()...))
  {};  // struct is_invocable

  static_assert(is_invocable<F>::value, "[Finally] The finally function must be callable with no arguments.");
#endif  // __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L

public:
  template <typename G>
  explicit Finally(G&& g)
#if defined(__cplusplus) && __cplusplus >= 201103L \
  || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \
  || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114))
    noexcept
#else
    throw()
#endif
    : m_f{std::forward<G>(g)}
  {}

  ~Finally()
  {
    m_f();
  }

  Finally(const Finally&) = delete;

  Finally&
  operator=(const Finally&) = delete;

  Finally(Finally&&) = delete;

  Finally&
  operator=(Finally&&) = delete;

  template <typename... Args>
  static void*
  operator new(std::size_t, Args&&...) = delete;

  template <typename... Args>
  static void
  operator delete(void*, Args&&...) = delete;

private:
  const F m_f;
};  // class Finally


#if defined(__cpp_deduction_guides)
template <typename F>
Finally(F&&)
  -> Finally<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


namespace
{
template <typename F>
#if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
#  if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
__attribute__((warn_unused_result))
#  elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_)
_Check_return_
#  endif  // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
#endif  // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
inline Finally<typename std::decay<F>::type>
makeFinally(F&& f)
#if defined(__cplusplus) && __cplusplus >= 201103L \
  || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \
  || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114))
  noexcept
#else
  throw()
#endif
{
  return Finally<typename std::decay<F>::type>{std::forward<typename std::decay<F>::type>(f)};
}
}  // namespace

テンプレートパラメータの F は戻り値 void(実は何でもよい),引数無しのラムダ式等の関数を想定している. C++14まではクラステンプレートの型推論はできないために,型推論のための関数テンプレート makeFinally を用意しておく. 戻り値の Finally クラスのインスタンスを無視してはならないので,[[nodiscard]] 属性を付与しておく. もし,返り値を無視した場合,関数呼び出し終了直後に指定したラムダが実行されるようになってしまう.

std::function を利用しないのは,std::function にラムダを格納してしまうとインライン展開が行われることが無くなることと,std::functionoperator() 自体の例外チェックコストにより,性能に大きな影響を及ぼすことがあるためだ. ラムダ式ラムダ式のまま保っておくことにより,インライン展開が期待できる. この記事では std::function を用いた実装が紹介されているが,説明のために簡略化したものであり,実際のコードで用いてはならない.

Finally クラスはコピー等を許可する必要が無いため,コピーctorやコピー代入演算子delete 指定しておく. また,new による動的確保は許可しないでおく.

このクラスは以下のように使用する.

#include <iostream>


int
main()
{
  auto f1 = makeFinally([]{
    std::cout << "Hello World! from finally\n";
  });

  {
    std::cout << "Foo\n";
    auto f2 = makeFinally([]{
      std::cout << "Bar\n";
    });
    std::cout << "Baz\n";
  }

  std::cout << "Hello World!\n";
}

このコードの実行結果は以下の通りである.

Foo
Baz
Bar
Hello World!
Hello World! from finally

変数 f1f2 が破棄されるタイミングでそれぞれ指定したラムダが呼び出されていることがわかる.

さて,この記事のタイトルにもあるfinally句としての Finally クラスの使用を限定すると,

try {
  // 処理
} catch (...) {
  // 例外処理
} finally {
  // finally処理
}

と書きたいコードは以下のように書くことができる.

{
  auto f = makeFinally([]{
    // finally処理
  });
  try {
    // 処理
  } catch (...) {
    // 例外処理
  }
}

具体例としては以下のようなコードになるだろうか. (かなり作為的ではあるが)

#include <iostream>
#include <vector>


int
main()
{
  std::vector<int> v{1, 2, 3, 4, 5};
  auto f = makeFinally([&v]{
    std::cout << "vector size: " << v.size() << std::endl;
  });
  try {
    std::cout << "v[0] = " << v.at(0) << "\n";
    std::cout << "v[10] = " << v.at(10) << "\n";  // 例外発生
    std::cout << "end\n";
  } catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
  }
}

Finally クラスは単にfinally句を実現する以外の用途にも転用可能である. よくあるのがC言語APIで,いくつかの初期化関数と終了処理関数をセットにして呼び出さなければならない場面だ. std::fopen()std::fclose() のような管理ポインタを返す関数であればスマートポインタを利用すればよい話であるが,全てが全てポインタを返却するような関数でないことがある.

例えば,C言語的な関数 init1()close1()init2()close2()init3()close3() が対応しているとしよう. そして,init1()init2()init3() の呼び出しの後に,目的の処理関数 doHoge() を呼び出す場面を考えると以下のようなコードになる.

void
func()
{
  if (init1() != SUCCESS) {
    return;
  }
  if (init2() != SUCCESS) {
    close1();
    return;
  }
  if (init3() != SUCCESS) {
    close2();
    close1();
    return;
  }

  doHoge();

  close3();
  close2();
  close1();
}

Win32 API等のバリバリのC言語関数を扱っているコードにこういうコードが見られることがあるかもしれない.

途中で1つでも失敗すれば,対応する終了処理関数を呼び出さなくてはならないため,同じ終了処理関数呼び出しを並べることになる. 宗教的・盲目的にgotoが禁止されていれば上記のようなコードを書かざるを得ないのである.

これを Finally クラスを用いて書き直すと以下のようになる.

void
func()
{
  if (init1() != SUCCESS) {
    return;
  }
  auto f1 = makeFinally([]{
    close1();
  });

  if (init2() != SUCCESS) {
    return;
  }
  auto f2 = makeFinally([]{
    close2();
  });

  if (init3() != SUCCESS) {
    return;
  }
  auto f3 = makeFinally([]{
    close3();
  });

  doHoge();
}

初期化処理のすぐ後に終了処理関数を書くことができ,途中で return したときの終了処理関数の呼び忘れを心配する必要が無くなる. スッキリとした記述になり,事故防止にもつながるのである. ちなみに,この例でわかる通り,Finally クラスはGoで言うところの defer に相当することがわかると思う.

余談

Finally クラスは継承を用いて実装することもできる.

#include <new>
#include <type_traits>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
Finally final
  : private F
{
private:
#if __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L
  static_assert(std::is_invocable_v<F>, "[Finally] The finally function must be callable with no arguments.");
#else
  struct is_invocable_impl
  {
    template <
      typename T,
      typename... Args
    >
    static auto
    check(T&& obj, Args&&... args) -> decltype(obj(args...), std::true_type{});

    template <typename...>
    static auto
    check(...) -> std::false_type;
  };  // struct is_invocable_impl

  template <
    typename T,
    typename... Args
  >
  struct is_invocable :
    public decltype(is_invocable_impl::check(std::declval<T>(), std::declval<Args>()...))
  {};  // struct is_invocable

  static_assert(is_invocable<F>::value, "[Finally] The finally function must be callable with no arguments.");
#endif  // __cplusplus >= 201703L || defined(_MSVC_LANG) && _MSVC_LANG >= 201703L

public:
  template <typename G>
  explicit Finally(G&& g)
#if defined(__cplusplus) && __cplusplus >= 201103L \
  || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \
  || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114))
  noexcept
#else
  throw()
#endif
    : F{std::forward<G>(g)}
  {}

  ~Finally()
  {
    F::operator()();
  }

  Finally(const Finally&) = delete;

  Finally&
  operator=(const Finally&) = delete;

  Finally(Finally&&) = delete;

  Finally&
  operator=(Finally&&) = delete;

  template <typename... Args>
  static void*
  operator new(std::size_t, Args&&...) = delete;

  template <typename... Args>
  static void
  operator delete(void*, Args&&...) = delete;
};  // class Finally


#if defined(__cpp_deduction_guides)
template <typename F>
Finally(F&&)
  -> Finally<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


namespace
{
template <typename F>
#if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
#  if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
__attribute__((warn_unused_result))
#  elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_)
_Check_return_
#  endif  // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
#endif  // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
inline Finally<typename std::decay<F>::type>
makeFinally(F&& f)
#if defined(__cplusplus) && __cplusplus >= 201103L \
  || defined(_MSVC_LANG) && _MSVC_LANG >= 201103L \
  || defined(_MSC_VER) && (_MSC_VER > 1800 || (_MSC_VER == 1800 && _MSC_FULL_VER == 180021114))
    noexcept
#else
    throw()
#endif
{
  return Finally<typename std::decay<F>::type>{std::forward<typename std::decay<F>::type>(f)};
}
}  // namespace

ただし,この場合は関数ポインタや final 指定のある関数オブジェクトは指定できない. ラムダとfinal 指定のない関数オブジェクトのみを受け取ることができる.

まぁ,ラムダ式を渡す場合がほとんどだと思うので,この実装でもさほど困らないと思うが.

まとめ

C++はctorとdtorによるRAIIにより,finally句が必要無い. この記事で紹介した Finally クラスは単なるfinally句だけではなく,もっと幅広いリソースの獲得と破棄処理に用いることができる.

多次元の std::array を楽に扱う

はじめに

前回の記事では,多次元の std::vector について書いた. 今回は多次元の std::array について書こうと思う.

まず,std::array は組み込み配列と同等の機能を提供するクラスである(というより,組み込み配列のラッパークラスである). 使用方法としては std::array<int, 4> arr のように,第1テンプレート引数に要素型,第2テンプレート引数に要素数を渡す. std::array<int, 4> arrint arr[4] に相当する宣言となる.

しかし,std::array を多次元にする場合を考えると,

std::array<std::array<std::array<int, 30>, 20>, 10> arr;

と宣言が長い宣言が必要になる. また,上記の3次元の std::array と同等の組み込み配列は int arr[10][20][30] であり,要素数の順が逆になっている.

この記事では,これらの問題を解決することについて書く.

実装

以下のコードがこの記事に書きたいことの全てである.

#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>


struct is_range_impl
{
  template<typename T>
  static auto
  check(T&& obj) -> decltype(std::begin(obj), std::end(obj), std::true_type{});

  template<typename T>
  static auto
  check(...) -> std::false_type;
};  // struct is_range_impl

template<typename T>
class is_range :
  public decltype(is_range_impl::check<T>(std::declval<T>()))
{};  // class is_range


template<
  typename R,
  typename T,
  typename std::enable_if<
    is_range<R>::value && !is_range<typename std::iterator_traits<decltype(std::begin(std::declval<R>()))>::value_type>::value,
    std::nullptr_t
  >::type = nullptr
>
static inline void
fill(R&& range, T&& value) noexcept
{
  std::fill(std::begin(range), std::end(range), std::forward<T>(value));
}


template<
  typename R,
  typename T,
  typename std::enable_if<
    is_range<R>::value && is_range<typename std::iterator_traits<decltype(std::begin(std::declval<R>()))>::value_type>::value,
    std::nullptr_t
  >::type = nullptr
>
static inline void
fill(R&& range, T&& value) noexcept
{
  for (auto&& e : range) {
    fill(std::forward<decltype(e)>(e), std::forward<T>(value));
  }
}


template<
  typename T,
  std::size_t kN,
  std::size_t... kSizes
>
struct ndarray_impl
{
  using type = std::array<typename ndarray_impl<T, kSizes...>::type, kN>;
};  // struct ndarray_impl

template<
  typename T,
  std::size_t kN
>
struct ndarray_impl<T, kN>
{
  using type = std::array<T, kN>;
};  // struct ndarray_impl

template<typename T, std::size_t kN, std::size_t... kSizes>
using NdArray = typename ndarray_impl<T, kN, kSizes...>::type;


int
main()
{
  NdArray<int, 4> arr1;
  fill(arr1, 0);
  for (const auto& e : arr1) {
    std::cout << e << " ";
  }
  std::cout << "\n\n";

  NdArray<int, 4, 4> arr2;
  fill(arr2, -1);
  for (const auto& e1 : arr2) {
    for (const auto& e2 : e1) {
      std::cout << e2 << " ";
    }
    std::cout << "\n";
  }
  std::cout << "\n";

  NdArray<int, 4, 4, 4> arr3;
  fill(arr3, 114514);
  for (const auto& e1 : arr3) {
    for (const auto& e2 : e1) {
      for (const auto& e3 : e2) {
        std::cout << e3 << " ";
      }
      std::cout << "\n";
    }
    std::cout << "\n";
  }
  std::cout << "\n";

  return 0;
}

std::array はテンプレート引数に要素数が必要なので,前回の記事NdVector とは異なり, NdArray<int, 10, 20, 30> とする必要がある. NdVector<int, 10, 20, 30> arr;int arr[10][20][30]; と同等であり,組み込み配列と遜色無い使用感になっていると思う.

なお,組み込み配列と同様に std::array 生成時に要素は初期化されないので,ローカル変数で使う分には初期化が必要となる. std::array は組み込み配列のみをメンバー変数に持つラッパークラスであり,全次元を通して領域が連続していることを利用して,

NdAarray<int, 3, 4, 5> arr;
std::fill(
  reinterpret_cast<int*>(arr.data()),
  reinterpret_cast<int*>(arr.data() + arr.size()),
  0);

のようにしてしまうのもアリだが,お行儀が悪いので,ちゃんとRange-based forでループを行って初期値を入れる関数 fill() を用意した.

なお,この fill() は他の多次元のものに対しても利用可能であるので,例えば多次元の組み込み配列にも適用可能である.

int arr[3][4][5][6];
fill(arr, 0);

当然,前回の記事の NdVector にも適用可能である. (各関数,クラスの実装は前回の記事を参照)

auto nv = makeNdVector(3, 4, 5, -1);

std::cout << "nv =\n[";
for (const auto& e1 : arr3) {
  std::cout << "  [\n";
  for (const auto& e2 : e1) {
    std::cout << "    [";
    for (const auto& e3 : e2) {
      std::cout << e3 << ", ";
    }
    std::cout << "],\n";
  }
  std::cout << "  ],\n";
}
std::cout << "]\n";


fill(nv, 0);

std::cout << "nv =\n[";
for (const auto& e1 : arr3) {
  std::cout << "  [\n";
  for (const auto& e2 : e1) {
    std::cout << "    [";
    for (const auto& e3 : e2) {
      std::cout << e3 << ", ";
    }
    std::cout << "],\n";
  }
  std::cout << "  ],\n";
}
std::cout << "]\n";

他にも多次元の std::list にも fill() を適用することが可能である.

別実装

いなむ神に以下のような実装を教えていただいた.

#include <array>


template<typename T, std::size_t N, std::size_t... Extents>
struct extents_expander
  : extents_expander<std::array<T, N>, Extents...>
{};  // struct extents_expander

template<typename T, std::size_t N>
struct extents_expander<T, N>
{
  using type = std::array<T, N>;
};  // struct extents_expander

template<typename T, std::size_t... Extents>
struct ndarray_helper
{
  using type = typename extents_expander<T, Extents...>::type;
};  // struct ndarray_helper

template<typename T, std::size_t N, std::size_t... Extents>
struct ndarray_helper<T[N], Extents...>
{
  using type = typename ndarray_helper<T, N, Extents...>::type;
};  // struct ndarray_helper

template<typename T>
using NdArray = typename ndarray_helper<T>::type;

これは以下のように使用できる.

NdArray<int[10][20][30]> arr;

こちらの方が直感的であると感じた.

まとめ

多次元 std::array の型の記述を簡単にするエイリアステンプレートの実装と,初期化関数を紹介した. 工夫すれば,多次元の std::array は多次元の組み込み配列と同じぐらい容易に扱うことが可能になる.