syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

Direct2D Effectsで発光する雷をプロシージャル生成する

Windows 8で使えるようになったDirect2D 1.1には、多数の便利な機能が追加されています。ブレンドモード(Composite Mode)の制御のほか、シェーダーを利用したエフェクトが使えるようになりました。
エフェクトは頂点シェーダーで実装されているとおぼしきアフィン変換や、ピクセルシェーダーで実装されているとおぼしきカラー変換、ドロップシャドウ、ブラーなどの便利なプリセットがあらかじめいくつか用意されているほか、ユーザープログラマーがHLSLで書いたカスタムシェーダーをもとにエフェクトを作成することもできるようです。役割的にはWPFのエフェクトに近いかもしれません。

無印Windows 7およびWindows Vista SP2 Platform Update (PU) 以降のOSに提供されている初代Direct2D 1.0では、ブレンドモードの制御ができなかったりして結局ちょっと複雑な画面効果となるともうお手上げでDirect3D連携するしかなかったのですが、Direct2D 1.1だとパラメータの設定だけで簡単に制御できるようになっています。もちろん加算合成もできるようになっているので、乱数を使ってプロシージャル生成したライトニング(稲光、稲妻、雷光、電光)に対してブルームエフェクト(グローエフェクト)を適用するテストをしてみました。

https://sygh-jp.github.io/content_hosting/my_program_ss/MfcD2DEffectTest1_ss_2014_08_07a.png

サンプルコード:GitHub - sygh-JP/DXGraphics_tests: Tests of DirectX Graphics by sygh

コーディングと動作検証は

で行なっていますが、Windows 7でもSP1 Platform UpdateであればDirect3D 11.1/Direct2D 1.1が一応使えるので動作すると思います。今回もMFCを使いましたが、Visual Studio 2010で追加されたDirect2D 1.0ラッパーは使わず、Direct2D 1.1 APIを直接叩いています。

雷光のライン生成アルゴリズムに関してはまだ改良の余地ありで、3次元に拡張する作業も残っているのですが、それっぽい見た目にはなっているんじゃないでしょうか。
最初は同じことをWPFのIM (Immediate Mode) でやってみようと思っていたんですが、エフェクトはともかくブレンドに関してはむしろDirect2D/Direct3Dで書いたほうがてっとり早いと気付いてしまったのでやめました。

ちなみにDirect3D 10/11を直に叩いた経験がある人だったら、Direct2Dのエフェクトが裏で何をやっているかだいたい想像がつくと思います。
Direct2Dを使ったコードがうまく動作しないという場合も、Direct3Dの経験があれば問題点を比較的簡単に特定できるでしょう。

※2018-05-06追記:
GitHubに移管したついでに、開発環境をVisual Studio 2015に移行しました。

Visual StudioのツールボックスにWin32/MFC向けのコントロール一覧が表示されないとき

これはときどき発生する現象で、Visual Studio 2008など以前のバージョンでも存在していた現象なのですが、今回Windows 8.1にインストールしたVisual Studio 2012/2013両方にて発生しました。
何が原因でこの現象が発生するのかはよく分かっていないんですが、[ツールボックス]ペインにて右クリックし、[ツールボックスのリセット]を実行することで解消できるようです。成功すれば[ダイアログ エディター]ノードが復活しているはずです。

MFCというかリソーススクリプトには結構古い設計を引きずっているがゆえの設計欠陥や不足機能が多々あるんですが、個人的にネイティブC++を使ったDirectXコンポーネントのテストやプロトタイピングにはまだMFCを使うことがよくあります。
ちなみに自分はWPF/WinRTではXAMLを直に書くため、デザイナーはほとんどプレビュー目的にしか使っていません。ツールボックスとかまったく使わないです。

OpenGLコンピュートシェーダー

(これは2013-01-30に書いた故OCNブログの記事を移植したものです)

2013-01-05に公開されたNVIDIAのリテールドライバー310.90で、ようやくOpenGL 4.3が正式に使えるようになりました。最新世代のKeplerのほか、旧世代のFermiアーキテクチャのグラフィックスカードでもフルサポートされてるのが嬉しいところです。

OpenGL 4.3の目玉はなんといってもコンピュートシェーダー (Compute Shader) でしょう。コイツはDirectXにおいてはバージョン11からDirectComputeとして搭載されていたGPGPU用シェーダーなんですが、OpenGLでもこのたびついに同様のシェーダーが搭載されることになりました。

早速サンプルコードを書いて実行してみました。

https://sygh-jp.github.io/content_hosting/my_program_ss/gl_cs_simple_test_ss02.png

といってもこのサンプル、実はココで公開されてるX Windowシステム用のコードを、FreeGLUTGLEWを使って書き直しただけです。ちなみに終了時のGLリソース解放コードとかは一切書いてないのでご注意を。なお、GLUTに関しては、Radeon環境やMac OS環境だとOpenGL 4対応の互換コンテキストを明示的に作成するようにする修正作業が必要かもしれません。コンテキストまわりを設定するならばGLUT系よりもGLFWGLUSのほうが良いと思います。

コンパイル・動作検証は

でやってます。FreeGLUTとGLEWは最新版を一式ダウンロード&インストールしてパスを通しておいてください。なお、GLEWはビルド済みバイナリが提供されていますが、FreeGLUTのほうは自前でビルドする必要があります。

サンプルのメインはgenComputeProg()グローバルメソッド内に文字列として記述されているGLSLコンピュートシェーダーのコード(CUDAやOpenCLでおなじみのいわゆるカーネル関数に相当)なんですが、何をやってるかっていうとカーネル関数に渡されたスレッドIDとかグループIDとかに適当な係数とフレーム時刻を乗じてOpenGLテクスチャに書き出し(ランダムアクセス)、それをポリゴンに張り付けてるだけの代物です。ちなみにimageLoad()/imageStore()によるテクスチャへのランダムアクセスはDirectCompute(HLSL)でいうTexture2D/RWTexture2Dのインデクサに相当する機能(StructuredBuffer/RWStructuredBufferではなく)のはずです。OpenGL 4.3のコンピュートシェーダーはDirectX 11のコンピュートシェーダーとは違い、DirectX 10世代のダウンレベルGPUでは機能制限どころではなくそもそも動かないものと思われます(現にFermiよりも古い世代ではドライバーが対応していない)。つまり、動作には必ずDirectX 11世代(シェーダーモデル5以上)のGPUが必須になります。
サンプルで使っているテクスチャはR32形式の浮動小数バッファ(GL_R32F)なので、RGBAのバックバッファに描画するとコンピュートシェーダーの計算結果がゼロ(R輝度がゼロ)の部分が補色のシアンになります(説明間違ってないよね?)。

なお、StructuredBuffer/RWStructuredBuffer (SB) 相当の機能はShader Storage Buffer Object (SSBO) と呼ばれるものなのですが、今回は省略します。あとAppendStructuredBufferやConsumeStructuredBufferに相当するものは見あたらないのですが、OpenGL 4.2のアトミックカウンターを使って各自なんとかしろ、ということかもしれません。また、ByteAddressBuffer/RWByteAddressBuffer (BAB) に関しても相当機能がなさげなんですが、もともとBABは頂点バッファやインデックスバッファ、Indirect系メソッドの引数用バッファなどをシェーダーで直接読み書きできるようにする類のビューです(D3D11_BIND_VERTEX_BUFFER, D3D11_BIND_INDEX_BUFFERやD3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS, D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSとは組み合わせられるが、D3D11_RESOURCE_MISC_BUFFER_STRUCTUREDとは組み合わせられない)。一方glDispatchComputeIndirect()用に専用のバッファGL_DISPATCH_INDIRECT_BUFFERを持ち、さらにVBOもSSBOとしてglBindBufferBase()で直接バインドできるOpenGLではどうやらBABは不要のようです。実際BABはかなり使いどころが難しいというかできれば使いたくないです……

#version 430

uniform float roll;
uniform image2D destTex;
layout (local_size_x = 16, local_size_y = 16) in;
void main() {
    ivec2 storePos = ivec2(gl_GlobalInvocationID.xy);
    float localCoef = length(vec2(ivec2(gl_LocalInvocationID.xy) - 8) / 8.0);
    float globalCoef = sin(float(gl_WorkGroupID.x + gl_WorkGroupID.y) * 0.1 + roll) * 0.5;
    imageStore(destTex, storePos, vec4(1.0 - globalCoef * localCoef, 0.0, 0.0, 0.0)); // Unordered access
}

ちなみにサンプルコード中ではGLSLのlayout修飾子を使ってローカルグループサイズ(HLSLでいうnumthreads属性)を指定していますが、実はOpenGLのコンピュートシェーダーにはGL_ARB_compute_variable_group_size(ARB_compute_variable_group_size)という拡張があって、GLSLコンパイル時だけでなくglDispatchComputeGroupSizeARB()によって実行時にもC/C++ホストプログラム側でローカルグループサイズを制御できるようになっているらしいです。この機能はDirectX 11のコンピュートシェーダー(DirectCompute)にはまだないのですが、CUDAやOpenCLには普通に同等機能が実装されていて、実行効率の向上やデータサイズに対する柔軟性を向上させるのに重要な機能なので、OpenGL 4アドバンテージの1つになっている模様。ダテに「DirectXを超えた」と謳っているわけではなさそうです。あと、シェーダー側でディスパッチグループ数・ローカルグループサイズを取得する場合、CUDAの gridDim, blockDim 組み込み変数や、OpenCL の get_num_groups(), get_local_size() 組み込み関数に相当するセマンティクスがHLSLにはなく、定数バッファなどを使って自前で通知するしかないのですが、OpenGLコンピュートシェーダーにはちゃんと gl_NumWorkGroups, gl_WorkGroupSize として実装されているのはありがたいです。まあAPIトータルで見ればDirectXのほうがきれいな設計になっていて圧倒的に開発しやすいんですが、こうして見るとDirectX 11もまだ欠点や機能不足を多数抱えていますね……なお、ホスト側でローカルグループサイズを指定する場合、シェーダープログラムの先頭に

#extension GL_ARB_compute_variable_group_size : enable

を記述し、さらにlayout(local_size_variable)を指定する必要があるようです。

あと記事を書いてて気付いたんですが、テクスチャのサイズは512x512なのにウィンドウクライアント領域のサイズが512x512じゃない……GLUTのglutInitWindowSize()はタイトルバーを考慮しないウィンドウ自体の横幅・縦幅を指定するものでしたorz

2015-02-21追記:
NVIDIAOpenGL/OpenGL ESのコンピュートシェーダーを使ったサンプルをいくつか公開しています。

AMDは2015年2月現在、OpenGL 4.3を一応サポートしているものの、依然としてOpenGLには真面目に取り組むつもりはないようです。MantleやDirectX 12がある以上、今更OpenGLに注力する価値はないと判断しているのでしょう(Windows以外は二の次)。IntelはHaswell/Broadwell世代のGPUOpenGL 4.3をサポートしているようです。

2018-05-26追記:
Visual Studio 2015とGLFWを使ってサンプルコードを書き直しました。ウィンドウクライアント領域のサイズも512x512に設定しています。

なお、GeForceドライバー388.13とGTX 760で試したところ、コンピュートシェーダー内で使用しているimage2Dオブジェクトには、writeonly修飾子を付けて定義しないとコンパイルエラーになりました。古いQuadroドライバー310.90はGLSL 4.3規格に正しく準拠していなかったのかもしれません。しかし相変わらずGLSLの文法は微妙な感じです。

*1:NVIDIAの310.90ドライバーはどうもBuggyらしいです。自分が確認しただけでも、MFC Feature Pack(というかBCGSoft製ライブラリ?)のGDIコントロール再描画がおかしくなるという現象が発生します。311系列以降を使うようにしたほうがよいです。

size_t, ptrdiff_tのprintf書式指定

(これは2010-12-05に書いた故OCNブログの記事を移植したものです)

size_tptrdiff_tはポインタ同様、32bit向け(_M_IX86とか)と64bit向け(_M_X64とか)でサイズと上下限が変わるプラットフォーム依存の整数型なので、printf()系の関数で出力する場合、書式指定(長さ修飾子、length modifier)に注意しないといけないのですが、MSVC(Visual C++)のCRTでは拡張書式として%Id, %Iuなどが用意されています。フォントによっては分かりづらいかもしれませんが、小文字のエルではなく大文字のアイです。

詳しくはMicrosoft Docs(旧MSDNライブラリ)のドキュメントを参照してください。

具体的な書式指定の仕方を挙げておきます。

printf("1L  << 16 by %%I32d = %I32d\n", 1L << 16);
printf("1LL << 48 by %%I64d = %I64d\n", 1LL << 48);
printf("INT32_MAX by %%I32d = %I32d\n", int32_t(INT32_MAX));
printf("INT64_MAX by %%I64d = %I64d\n", int64_t(INT64_MAX));
printf("-1 by %%I32u = %I32u\n", uint32_t(-1)); // UINT32_MAX と同値。
printf("-1 by %%I64u = %I64u\n", uint64_t(-1)); // UINT64_MAX と同値。
printf("PTRDIFF_MAX by %%Id = %Id\n", ptrdiff_t(PTRDIFF_MAX)); // ターゲット プラットフォーム依存。
printf("SIZE_MAX    by %%Iu = %Iu\n", size_t(SIZE_MAX)); // ターゲット プラットフォーム依存。

// %Id, %Iu はそれぞれ ptrdiff_t, size_t 相当の型(ポインタ互換整数型)に対して使う。
// %ld, %lu はそれぞれ signed long, unsigned long 用。Win32/Win64 では int と long のサイズが同じなので %d, %u と違いはないが、Win16 では区別される。
// signed long long および unsigned long long にはそれぞれ %lld, %llu を使う。Win32/Win64 では 64bit 整数になる。

int32_tやint64_tなどはC99やC++11 (C++0x TR1) の <stdint.h>, <cstdint> で定義されている、処理系ごとにサイズ保証された型なんですが、VC++では2010以降で実装されています。

ちなみに-1を符号無し整数型にキャストすると、その型の最大値となります。

その他、Windows APIでよく見かけるINT_PTR、UINT_PTR、LONG_PTR、ULONG_PTR、DWORD_PTRなども、単に「ポインタと相互変換してもいい整数型(アドレスを格納できるだけの幅を持った整数型)」という意味のプラットフォーム依存型なので注意しましょう。.NETでいうとSystem.IntPtrSystem.UIntPtrに相当します。C99/C++11ではオプションとして intptr_t, uintptr_t という型が標準化されていますが、これらも同じです。

まぁ16進数表記でかまわないんであれば、size_tもptrdiff_tも従来の標準規格に沿った%p書式指定を使って出力すればよいです。

あと、こういう環境依存の型に対する操作は、C++のストリームのほうが本質的に型安全で、楽といえば楽です。C/C++で可変引数(可変長引数、可変個引数)を使う場合、うかつに変数の型を変えられないというデメリットがあります(必ず対応する書式も注意深く修正する必要があります*1)。

Boost C++ライブラリが使える環境では、Boost.Formatを使うという手もあります。

なお、gccでは%Id, %Iuの代わりに%zd, %zu, %td, %tuなどの書式が使えるようです。これらはC99規格の書式らしいです。

他にも、C99では処理系における最大サイズの整数を格納できる intmax_t, uintmax_t が定義されています。対応する長さ修飾子はjで、それぞれ%jd, %juを使います。
ユーザー定義のtypedef整数型エイリアスは、具体的な型が何に展開されるか定まっておらず、処理系が変わると書式と適合しなくなるおそれがあるので、printf()/scanf()系の引数に直接渡すべきではありません。
short, int, longなどの組み込み型もしくは標準規格で定義されているエイリアス型を使うべきです。

C99では signed char / unsigned char を出力するための長さ修飾子hhも追加されています。C/C++において、int未満のサイズを持つ型の値を可変引数に渡すと、「既定の実引数拡張」のルールによっていったんsigned intもしくはunsigned intに型昇格されるのですが、このため負数を16進数で出力する場合は注意が必要となります。C99より前の規格でも、signed shortに関しては長さ修飾子hがあったので特に問題なかったのですが、signed charに関するサポートがなく、いったんunsigned charにキャストしてから可変引数に渡す*2などの面倒な対処が必要でした。

printf("SCHAR_MIN = 0x%x\n", static_cast<signed char>(SCHAR_MIN)); // 大抵の処理系で 0xffffff80 になる。
printf("SCHAR_MIN = 0x%x\n", static_cast<unsigned char>(SCHAR_MIN)); // 大抵の処理系で 0x80 になる。
printf("SCHAR_MIN = 0x%hhx\n", static_cast<signed char>(SCHAR_MIN)); // 大抵の処理系で 0x80 になる。

既定の実引数拡張に関しては以下も参照してください。

MSVCがなぜC99をサポートしないのか、なぜ独自拡張にこだわるのか、詳しいことはよくわからないんですが、64bit版WindowsLinux系とは違ってLLP64モデルであること、Windowsネイティブ開発における事実上標準言語がC++であることに起因しているような気がします。C言語オープンソース界隈とかは、MSVCがC言語のサポート強化を怠けてる状況に憤りをおぼえているかもしれません(クロスプラットフォームなライブラリを作る際は一番遅れている処理系に合わせた規格でコードを書かないといけないので、あるOS上での事実上標準の処理系が他に遅れをとっていると非常に困る)。
しかし、C言語によるコード生産性はすでに他の言語と比べると大きく遅れをとっており、せいぜいアセンブラよりはマシといった程度でしかないので、WindowsプログラマーにとってC++(もしくはC#)の修得は必須ですが、Cはほとんど利用する場面がありません(ドライバー開発は除く)。それゆえ、MSにとってCはもはや死んだ言語同然なのだと思われます。ちなみに個人的には

C#の文法>>C++の文法>>>>>>>>>>Cの文法」

くらいの差があると考えています。文法仕様の品質・完成度は言語機能同様、コードの生産性やメンテナンス性に大きく影響を及ぼします。

※2020-11-24追記:
VC2013までは hh, t, z, j の長さ修飾子が実装されていませんでしたが、VC2015以降は実装されているようです。
ATLTRACE()CString::Format() でも使えます。

C++11規格では標準ライブラリの大半がC99互換になっています。

*1:学生の頃、16bit環境向けプログラムの変数の型を一部intからlongに変えてサイズ拡張する作業をやらせてもらったことがありましたが、そのときに対応する書式をきちんと変えなかったため、データが正常に出力されなくなるバグを混入させてしまったことがあります。

*2:この方法は、2の補数系でない場合は通用しないかもしれません。

C++正規表現ライブラリ(Regex)でCSV解析

(これは2011-01-30に書いた故OCNブログの記事を移植したものです)

C++11 (C++0x TR1) には正規表現ライブラリとして <regex> が存在するんですが、こいつを使って簡単なCSV文字列解析処理を書いてみました。参考にしたのは下記。
CSV形式のファイルをDataTableや配列等として取得する: .NET Tips: C#, VB.NET

解析用の末尾コンマをあらかじめ追加しておくこと、そして正規表現のみで解決しようとせず、C++によるループを利用して正規表現を繰り返し適用しているのがコツです。
一応Visual C++ 2008 SP1 / 2010以上の環境を想定していますが、g++などでもコンパイルできるように修正するのはそんなに難しくないはずです。ちなみにC++11のRaw string literalsが使えるコンパイラであれば、複雑な正規表現も記述しやすくなります。

#include <regex>
#include <vector>
#include <tchar.h>
#include <windows.h>
#include <atlbase.h>
#include <conio.h>

typedef std::basic_string<TCHAR> tstring;

bool ParseCsvLine(LPCTSTR pCsvLine, std::vector<tstring>& outArray)
{
    if (!pCsvLine)
    {
        return false;
    }

    try
    {
        const std::tr1::basic_regex<TCHAR> re(_T("\\s*(\"(?:[^\"]|\"\")*\"|[^,]*)\\s*,"));

        std::tr1::match_results<tstring::const_iterator> results;

        tstring strLine(pCsvLine);
        if (!strLine.empty() && strLine[strLine.length() - 1] != _T(','))
        {
            // 解析用末尾コンマを強制追加する。
            strLine += _T(',');
        }
        tstring::const_iterator start = strLine.begin();
        tstring::const_iterator end = strLine.end();
        while (std::tr1::regex_search(start, end, results, re))
        {
            outArray.push_back(tstring(results.str(1)));
            start = results[0].second;
        }

        // 解析の結果、たとえマッチした要素がひとつも無くても true を返す。
        return true;
    }
    catch (const std::exception& err)
    {
        err; // Warning C4101 対策。
        ATLTRACE("Exception[%s] : %s\n", typeid(err).name(), err.what());
        return false;
    }
}

void main()
{
    _tsetlocale(LC_ALL, _T(""));

    LPCTSTR pTargetCsvText = _T("foo, foo,long long,123.4,\"bar\",日本語, ,\" \",\",\",\"(x, y, z)\",,ソース");
    std::vector<tstring> csvColumnsArray;

    _tprintf(_T("Target CSV Text = \"%s\"\n"), pTargetCsvText);
    ParseCsvLine(pTargetCsvText, csvColumnsArray);
    printf("CSV Columns Count = %Iu\n", csvColumnsArray.size());
    for (size_t i = 0; i < csvColumnsArray.size(); i++)
    {
        const tstring& str = csvColumnsArray[i];
        _tprintf(_T("[%02Iu] = \"%s\"\n"), i, str.c_str());
    }
    puts("Press any...");
    _getch();
}

shared_ptrとconst

(これは2011-01-17に書いた故OCNブログの記事を加筆修正したものです)

あんまり言及してる人がいないんですが、これはshared_ptrとconstを組み合わせて使う上でとても重要な機能です。

普通組込のconstポインタあるいはconst参照経由では、const操作しかできません。

例えば、

class Vector3F
{
public:
  float x, y, z;
public:
  Vector3F() : x(), y(), z() {}
  Vector3F(float inX, float inY, float inZ) : x(inX), y(inY), z(inZ) {}
public:
  // ベクトルの長さを返す。※内部状態を変更しない。
  float GetLength() const
  { return sqrt(x * x + y * y + z * z); }
  // 自身を正規化する。※内部状態を変更する。
  void Normalize()
  {
    const float len = this->GetLength();
    if (len > 0)
    {
      this->x /= len;
      this->y /= len;
      this->z /= len;
    }
  }
};

なクラスがあったとして、

Vector3F vec(1, 2, 3);
const Vector3F* pReadonlyVector = &vec;
float len1 = pReadonlyVector->GetLength();
const Vector3F& rReadonlyVector = vec;
float len2 = rReadonlyVector.GetLength();

はOKだけど、

pReadonlyVector->Normalize();
rReadonlyVector.Normalize();

コンパイルエラーになります(今回、変数名のpプレフィックスやrプレフィックスは便宜上付けているだけですのであしからず)。

こういう「内部状態を変更しない」ことをコンパイル段階で表明し、堅実なプログラムを記述するのに非常に役立つのがC++のconst修飾子(const modifier)であり、C#Javaにはない優れた機能だと思います*1
C++プログラマーのスキル レベルやライブラリの品質を評価する場合、このconstをきちんと理解して使っているかどうかが一つの指標になります。というかconstをきっちり使っていないC/C++ライブラリなんて怖くて使う気になれません。

ここで、型Tのconst組込ポインタ型に対応するスマートポインタとして、Boostライブラリのboost::shared_ptr、C++0x TR1のstd::tr1::shared_ptr、もしくはC++11のstd::shared_ptrでconstスマートポインタ型を作るのであれば、

typedef shared_ptr<const T> TMyConstSharedPtr;

あるいは

typedef shared_ptr<T const> TMyConstSharedPtr;

とします。このconstスマートポインタ型からはconst操作しかできなくなります。

なお、constでないスマートポインタ型はconstスマートポインタ型に暗黙的な代入が可能となっています。これはちょうど下記のように、constでない組込ポインタ型がconst組込ポインタ型へ暗黙的な代入が可能になっているのと同じです。

Vector3F vec(1, 2, 3);
Vector3F* pVector = &vec;
pVector->Normalize();
const Vector3F* pReadonlyVector = pVector;

こういった通常のプリミティブ型に用意されている機能をそのまま使えるように模倣してくれているところがC++テンプレート ライブラリの醍醐味と言えます。

なお、const shared_ptr<T> と shared_ptr<const T> の違いは下記を見れば一目瞭然です。

#include <iostream>
#include <memory>
#include <vector>
#include <cassert>

typedef std::vector<int> TIntArray;
typedef std::shared_ptr<TIntArray> TIntArraySharedPtr;
typedef std::shared_ptr<const TIntArray> TIntArrayConstSharedPtr;

void Print(const TIntArrayConstSharedPtr& inArray)
{
  assert(inArray);
  for (auto x : *inArray)
  {
    std::cout << x << std::endl;
  }
  //inArray->clear(); // Compile error will occur
}

void Multiply(const TIntArraySharedPtr& inoutArray, int opMul)
{
  assert(inoutArray);
  for (auto& x : *inoutArray)
  {
    x *= opMul;
  }
  //inoutArray->clear(); // No comile error
  //inoutArray = std::make_shared<TIntArray>(); // Compile error will occur
}

int main()
{
  TIntArraySharedPtr myArray = std::make_shared<TIntArray>();
  for (int i = 0; i < 10; ++i)
  {
    myArray->push_back(i);
  }
  Multiply(myArray, 2);
  Print(myArray);
  return 0;
}

shared_ptrとキャスト

ちなみに派生クラスのshared_ptrは基底クラスのshared_ptrにそのまま代入(アップキャスト)することができますが、基底クラスのshared_ptrから派生クラスのshared_ptrへダウンキャストしたり、基底クラスAのshared_ptrから基底クラスBのshared_ptrへクロスキャストしたりする場合(組込ポインタであればstatic_castやdynamic_castを使う場面)は、static_pointer_cast()関数やdynamic_pointer_cast()関数を使用します。

#include <cstdio>
#include <memory>

class BaseA
{
public:
  virtual ~BaseA() {}
};

class BaseB
{
public:
  virtual ~BaseB() {}
};

class Derived : public BaseA, public BaseB
{
public:
  Derived() {}
};

int main()
{
  using std::shared_ptr;

  shared_ptr<Derived> pDerived1 = std::make_shared<Derived>();
  printf("pDerived1 = 0x%p\n", pDerived1.get());
  shared_ptr<BaseA> pBaseA = pDerived1; // アップキャスト。
  printf("pBaseA = 0x%p\n", pBaseA.get());
  shared_ptr<Derived> pDerived2 = std::static_pointer_cast<Derived>(pBaseA); // ダウンキャスト。
  printf("pDerived2 = 0x%p\n", pDerived2.get());
  shared_ptr<BaseB> pBaseB = std::dynamic_pointer_cast<BaseB>(pBaseA); // クロスキャスト。
  printf("pBaseB = 0x%p\n", pBaseB.get());

  // 継承関係が明らかな場合は static_pointer_cast を使える。
  // 実行時に継承関係をチェックする必要がある場合、dynamic_pointer_cast を使う。

  return 0;
}

ちなみにconst_castへのアナロジーとして、同様にconst_pointer_cast()関数も存在しますが、一応用意されているというだけなので使う機会はまずないでしょう(そう願いたい)。

shared_ptrとインテリセンス

VC++ 2008 SP1にはstd::tr1::shared_ptr(<memory>ヘッダーをインクルードすることで使用可能)が実装されていて、これはboost::shared_ptr(<boost/shared_ptr.hpp>ヘッダーをインクルードすることで使用可能)とほぼ同じものなんですが、どちらもヘッダーをインクルードした途端なぜかインテリセンス(コード補完機能)が効かなくなることがあります。こういうとき、「追加のインクルード ディレクトリ」に「./」もしくは「.\」(カレント ディレクトリの相対パス)を追加しておくと、インテリセンスがちゃんと効くようになることがあります。詳しいことはよく分かっていません。VC++ 2012などではstd::shared_ptrを使うときもちゃんと普通にインテリセンスが機能するので、一応この問題は修正されているように思います。

余談ですが、こういうディレクトリ パスやファイル パスをプロジェクト プロパティで設定するとき、スラッシュ(0x2F)とバックスラッシュ(0x5c)どちらを使ってますか? 自分はVisual C++環境変数($(SolutionDir)など)を組み合わせるときはバックスラッシュを使って、そうでないときは(UNIX環境と互換性があり、さらに日本語Windows環境で語境界が分かりやすい)スラッシュを使うようにしています。日本語環境ではフォントによってはバックスラッシュが円記号になるので、かなり見にくく(醜く)なります。UNIX標準はパス区切りがスラッシュなんですが、Windowsでは基本的にパス区切りがバックスラッシュではあるものの、アプリケーションやAPIによってはスラッシュもバックスラッシュも両方使える場面があります。あと、自分はC/C++の#includeディレクティブに指定するパス名には、必ず区切り文字にスラッシュを使うようにしていますが、Visual C++のリソース スクリプト(*.rc)では標準でバックスラッシュが使われてたりするので、リソース スクリプトを直接編集する場合はバックスラッシュに合わせています。区切り文字にスラッシュもバックスラッシュも許可する仕様にしてしまったWindowsなんですが、こういうあいまいな仕様はやめて欲しかったです。あとASCIIバックスラッシュをフォントによって円記号に割り当てることを最初に考えた人は本当におバカだなと思いました。

ちなみにシェルのPath系APIや、そのラッパーであるATL::CPathのメソッドには、入力文字列中にスラッシュとバックスラッシュを混ぜるとおかしな結果になるものもあるので、Windows環境ではできるかぎりバックスラッシュで統一することをお勧めします。

*1:C#にはconstとreadonly、Javaにはfinalがありますが、C#のconstはコンパイル時の完全定数のみ、readonlyはコンストラクタのみ、Javaのfinalは参照型の内部状態変更には関与しないなど、C++のconstほどの強力さはありません。

C++のmutableの使い道

(これは2010-10-20に書いた故OCNブログの記事を加筆修正したものです)

C++mutableconstメソッド(constメンバー関数)内でも変更可能なフィールドを定義する場合などに使います。mutableはヘタに使うと混乱を招くだけの余計な機能なんですが、以下のような感じでスレッドセーフなクラスを実現する際に使うことができます。

template<class T, class Container = std::queue<T> >
class CThreadSafeQueue {
private:
    mutable CCriticalSection m_cs; // MFC の同期オブジェクト。
    Container m_queue;
public:
    void Push(const T& data) {
        CSingleLock lock(&m_cs, TRUE);
        m_queue.push(data);
    }
    bool Pop() {
        CSingleLock lock(&m_cs, TRUE);
        if (m_queue.empty()) {
            return false;
        }
        else {
            m_queue.pop();
            return true;
        }
    }
    bool GetFront(T& info) const {
        CSingleLock lock(&m_cs, TRUE);
        if (m_queue.empty()) {
            return false;
        }
        else {
            info = m_queue.front();
            return true;
        }
    }
    bool GetBack(T& info) const {
        CSingleLock lock(&m_cs, TRUE);
        if (m_queue.empty()) {
            return false;
        }
        else {
            info = m_queue.back();
            return true;
        }
    }
    size_t GetSize() const {
        CSingleLock lock(&m_cs, TRUE);
        return m_queue.size();
    }
};

CCriticalSectionはboost::mutexやstd::mutexに相当します。
CSingleLockはboost::lock_guardやstd::lock_guardに相当します。

ここで重要なのはGetFront(), GetBack(), GetSize()のconst修飾子です。const修飾子の付いたメソッド内では、フィールド(メンバー変数)に対する変更を行なうことができなくなり、これによって「constメソッドであればオブジェクトの内部状態に変更を加えることがない」というコンパイラ保証を付加することができるため、より安全かつ可読性の高いコードを書くことが可能となるのですが、スレッドセーフなクラスを作るときはメソッド呼び出しをスレッドセーフにする*1ための同期オブジェクト(ロックオブジェクト)を非constで扱う必要があり、衝突することになります。これを回避するために、クラスオブジェクトの本質的なコンテキストとは関係ない同期オブジェクトのメンバーはmutableにしておきます。

なお、通常size_t自体の読み出しと書き込みは分割されることがないため、GetSize()は別にロックしなくてもよさそうに見えるかもしれませんが、規格上ロックは必須です。ロックなしだと「他のスレッドでの非const操作中に呼び出してもよい」という保証はsize_tやstd::queueの内部実装に依存する羽目になります。単純なsize_tの読み出しだけであれば通例32bit版でも64bit版でも1命令で実行できる操作(≒アトミック操作)となるため、一見するとロックなしでもよさそうですが、size_tが32bit環境において32bit幅である保証や、64bit環境において64bit幅である保証はどこにもありません*2。また、内部的に保持・読み書きしているデータが構造体型だったりすると話が変わってきます。書き換え途中の半端な状態を観測してしまう可能性があるからです。std::queue::size()の計算量は定数時間とは限らず、リストの要素をたどって数え上げる実装になっているかもしれません。要素を追加/削除している途中の状態を無理やり他のスレッドから読み取ってしまうと、ダングリングポインタ経由で不正なメモリを参照してしまうこともありえます。このようにコンテナの内部実装に依存するようなコードを書くべきではありません。仮にstd::queue::size()がメンバー変数にキャッシュされた値を読み取って返す実装になっていたとしても、そもそも複数のスレッドからの読み書きアクセスがアトミックでもなく排他制御されてもいない場合、データ競合(data race)となり、未定義動作を引き起こします。未定義動作ということはつまり、規格上C++コンパイラはどのようなコードを出力してもよくなるので、最適化の結果まったく意図しない動作をするコードが出力される可能性もあります*3

ちなみに、C++11のconstメソッドは、constメソッドのみを呼び出す場合に関してはスレッドセーフ性も表明することになります(排他制御を行なうか、ロックフリーであるかは問わない)。記憶にとどめておく必要があるでしょう。

*1:共有変数に複数のスレッドから読み書きアクセスされる可能性のあるコード(少なくとも一方が書き込みアクセスであるコード)は、明示的に排他制御しておかないとデータ競合の未定義動作を引き起こします。

*2:さらに言うと、32bit/64bitアーキテクチャにおいて32bit/64bitの値が1命令で読み書きできるかどうかは、そのデータのアライメントにも依存します。例えば強制的にパディングを無くしてパッキングされた構造体メンバーなど、4バイト/8バイトのアライメント境界をまたいで配置されているようなデータの場合、1命令で読み書きすることはできません。

*3:コンパイラ最適化による命令の入替(リオーダー)だけに限らず、アウトオブオーダー実行のようにCPUによって命令の入替が実施されることもあります。