空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

C++でDNNが使えないなんてあるわけないよ in Windows

一ヶ月くらいクソ忙しかったせいで久々の更新。
書くネタは山ほどあるんだけど、またその内、連続で書きます。

はじめに

Windows環境のC++(これ重要)でDNN(Deep Nueral Network)を使おうぜって話。
DNNといえば最近流行りのものなんだけど、如何せんWindows環境。特にC++となるとグッと道が狭まる傾向にある。
正直嫌がらせでも受けてるんじゃないんですかねって言うくらいに。

実際、有名な所でGoogleのTensol Flowがあるけれども、あれもMacLinuxの64bitしか対応していない。
それで色々とC++で使えるDNNののライブラリを調べてみたわけです。
個人的な要望として、GPGPUによる高速化が成されているものがベストかなと思っています。

Tensor Flow

Googleが出した商用フリーの機械学習ライブラリで、なんかすごいらしい。
曰く、状態遷移図で表現できるあらゆる問題を解けるとか凄い話を聞いたことがある。
私は現在Windows以外のまともな環境が無いので使ったことはないけれども、学習の経過をビジュアライズまで自動でやってくれて、その結果をブラウザで確認できるのがすごく魅力的。
ただし、前述の通りWindowsでは使えないのです。

OpenCV

おそらく最も有名無い画像処理/コンピュータビジョンのライブラリ。
なのだけれども、いくら探してもOpenCVだけでDNNを使うという話が出てこない。
より正確に言うとC++OpenCVだけを使ってDNNを使うという話が出てこない。
実際に「OpenCV DeepNeuralNetwork」で検索しても上から10個が全てCaffeのモデルを読み込んでみた、という内容のものばかりだし、OpenCVのサンプル一覧を見てもそれらしいものが見あたらなかった。

DLIB

以前記事をかいたけれども、機械学習ライブラリでその中にDNNも含まれており、もちろんGPGPUを用いた高速化も可能。
なのだけれども、残念なことにWindows環境だとリンクエラーがでる。
FAQ曰く、Visual Studioの場合C++11に完全に対応しているバージョンがないので未対応とのこと。
さらに、こいつ。厄介なことに全てのパラメータがテンプレートで解決されるため、動的にパラメータを変えてどうのこうのっていうのはやり難い。
まあ、最近はAutoEncoderかなにかで事前学習して層数とかを決めるらしいからそれもあまり問題にならないのかもしれないけれども。

Tiny DNN(https://github.com/tiny-dnn/tiny-dnn)

非常にスマートなライブラリで、Windowsに対応している素敵ちゃん。
必要なライブラリとしてOpenCVを要求していることは入力インターフェースにOpenCVを使っていると思われる。
特徴としては、DLIBと違ってパラメータを動的に決定することが可能。
ただし、CPUでの学習しか対応していないので、その辺りはネックに成るかもしれない。

えっ、無くね?って話

Tiny DNNが一番良いのだけれども、GPGPUによる高速化がないと学習が終わる気しない。
Tiny DNNのトップページ曰く、

98.8% accuracy on MNIST in 13 minutes training (@Core i7-3520M)

とのことなので、6000サンプル10クラスよりも多いクラスに分けたり、サンプル数を増やしたりとしていると爆発的に計算時間が掛かりそう、というか掛かるのでやっぱりGPGPUが良い。

DLIBさんのDNNってなぜに動かないのん?

FAQに書かれている内容は以下の通りだが、どうにも納得できない。

The deep learning toolkit in dlib requires a C++11 compiler. Unfortunately, as of July 2016, no versions of Visual Studio fully support C++11, so not all the deep learning code will compile. However, all the other modules in dlib can be used in Visual Studio without any trouble.

確かに、Visual Studio 2015は結構C++11の中で対応していない機能がある。
ただ、それもUpdate 2でかなり解消されている。

それで、実際にソースコードを読み漁ってみてもどこにもそれらしいコードは見られなかった。
というわけで無理やりコンパイルしてしまえという結論に至りました。

DLIB with DNN in Windows

DLIBをDNNが使える状態でVisual Studioにてコンパイルするためにはcmakeをするときに以下のオプションを足せばOKです。

cmake -DCOMPILER_CAN_DO_CPP_11 <dlibのルートディレクトリ>

これで後は普通にコンパイルするだけです。
実際にライブラリをリンクしてコンパイルしても問題なく動作することを確認しています。

ただし、未だnvccでコンパイルが通るかどうか確認していません。
というのもCUDA8.0 RC ですらVisual Studio 2015 Update 1までしか対応していなくて、現在、私が持っている環境はUpdate2とUpdate3なので、両方共CUDAをONにしてdlibのコンパイルを出来ていません。
なので、上記の話は全てCPUで使う場合にのみ有効です。

CUDAについては、おいおい動作確認をしていきます。
それとサンプルコードも実はすでに書いたのですが、それはまた後日別記事であげますので悪しからず。

追記

今朝方、上記の方法にてDLIBをVisual Studio 2015 Update2でコンパイルしてみるとコンパイルに失敗したので、おそらくCUDAを使うことは難しそうです。

追記2

私がこの記事を上げてから、ほぼ同時か、翌日くらいにgithubにissueが上がっていたので追記。
どうやらdlib::repeatを使ってDNNのノードを定義すると、テンプレートの展開で無限ループにハマるらしくVS2015 Update3ではまだDNNを使えないようにしているらしい。

追記3

実際にDLIBを使ってDNNを使ってみたサンプル&解説記事を書きました。
elda27.hatenablog.com

GPGPUって色々あるけど結局どれがいいの

はじめに

元ネタはこれ。本文の引用も特に注意書きがなければここから引用してます。
CUDAとOpenCLどっちがいいの? - Qiita

オチは変わらず、

そもそも単純に比べんな。ナイフとノコギリがどっちがいいかなんて一概には言えないだろう?

ただ、自分なりに色々調べたり使ってみて、それのまとめ。

CUDA

元ネタにかかれている通り、GPGPUっていうとCUDAが最強って説が多いし、実際使ってみるとめちゃくちゃ早い……らしい。
それに、オフラインコンパイルができるのがかなり素敵。
ただ、最新のCUDA8.0RC版ですら、Visual Studio 2015のUpdate 1までしか対応してないので、常に最新の環境を使いたい人や何も考えずにVisual Studioをアップデートしてきた人にとってはかなり辛い(2016年8月18日現在Update 3まで出ています)。
まあ、常に最新の環境を使いたい人はclang使えよって思うから一概には言い難いけど。

それはさておき、私は何も考えずにVisual Studioをアップデートしてきた勢なんでCUDAは全く使っておりません(他所の環境で遊ばせて貰ったことはありますが…)。

OpenCL

OpenなGPGPUというか並列化ライブラリ代表で、コンピュータビジョンやってる人的にはOpenCVのラッパーが結構便利で利用しやすいのが売りかな。
それと、GPUが載っていない環境でもOpenCLの場合、CPUでも同一のソースコードで並列化が実現できるので、その点結構便利かもしれない。
ただ、如何せん規格が微妙という説がある。
以下は個人的に使ってみた感想というか不満。

・オフラインコンパイルができないせいで実行時に毎回コンパイルする必要がある。
・元ネタにも書いているけどnVidiaのやる気の無さがパない。具体的にはGTX 970のドライババージョン350.81(だったかな)でOpenCL1.2のそれも半端にしか対応してない。これがどれ位酷いかと言うと、2005年頃にノートパソコン向けで一線級を誇っていたAthron X2 QL-60ですらOpenCL1.2に対応しているので、推して知るべし。
・元ネタを忘れたんだけど、初期の頃、中間言語がないため、実装をベンダーに丸投げしたせいで結構昔からあるくせにあまり浸透しなかったらしい。さらに、ベンダーによって実装レベルがマチマチ&nVidiaさん見たいなやる気無し勢が発生する原因にもなっているらしい。

OpenGL Compute Shader

Vertex ShaderやFragment Shaderは使ったことあるけど、これは使ったことない。でも、書き方は一緒なので多分使えるんだと思う。
ただ、OpenGLもShaderのオフラインコンパイルができないので実行時に毎回コンパイルする必要があるから、OpenCL使える人はわざわざこれ使う意味はないと思う。

DirectX Direct Compute

こいつもOpenGL Compute Shaderと一緒。ただ、こいつはオフラインコンパイルができるからOpenGL Computer Shaderよりも遥かにまし。

C++ AMP

もう最高。配列に突っ込んで計算するだけでハイ爆速っていうのが最高。
ただし、以下のとおり、Direct Computeが動いているのでDirectX 11の機能レベル11.0以上が必要。

Microsoftがやってるので当然Windowsで動きます(裏ではDirectComputeが動いているっぽいです)

さらに、倍精度演算を行うためにはWDDM1.4以上に対応している必要があったはず(うろ覚え)。

なお、私のメインの開発PCはDirectX 11対応のくせに機能レベル10.0とかいう謎仕様なので、使えません。

HIP

CUDAとほぼ同じ構文でポータブルな感じにしてくれるもの。 HIP自体はフロントエンドコンパイラみたいなもので、バックエンドにnvccを使えばCUDAに、先述のHCCを使えばGCN-ISAかHSAILになります。どちらも、オーバーヘッドはありません。もし更に最適化したかったら変換後のコードを見て最適化が可能です。

らしいのだけれども、Windowsに対する嫌がらせなのかWindows環境だとCMakeが通らない。
どうやら、CMake内でPerlスクリプトを使ってバージョン情報やらを取得してるんだけど、それの中でエラーが起きて上手くバージョン情報やらを取得できてないっぽい。
それに、私のコンパイル方法が悪いのかと思ってggってもHIPっていう名前のせいで尻についてしか出てこず、碌な情報がて手に入らないのがかなり辛い。

謳っていること自体はかなり私好みなだけに本当に辛い。

Etc...

使ったことも使う予定もないので知らん。
元ネタでも書かれているとおり調べても情報が出てこないか、微妙だなという話が非常に多い。

おわりに

私の開発PCはAMDなのでできたらHIPを使いたかったのだけれども、どうにも動かないので結局OpenCLを使い続けてます。
どなたか、HIPの情報まとめてくれないかなー

OpenGLでズームを実装する方法

OpenGLでズームを実装する方法について。
最初、カメラの中心と距離をいじってやろうと思ってたんだけど、どうも上手くいかなかったから、ちょっと調べたら出てきたました。
元のネタはここの8.040、How do I implement a zoom operation?
https://www.opengl.org/archives/resources/faq/technical/viewing.htm

最も手っ取り早い方法としてModelView行列の対角要素をN倍する方法があるけど、それはスケールが大きく/小さくなりすぎると、Projection行列のNearかFarの影響で描画したいものが正確に表示されないという問題がある。そこで、ModelView行列ではなく、Projection行列を弄ってやればハッピーじゃないかという話です。

例えば、glFrustum、ここではもう少しモダンOpenGLということでglm::frustumですが、それを使ってズームを実装すると以下のようになります。

glm::frustum(left / scale, right / scale,
    bottom / scale, top / scale,
    z_near, z_far);

ただ、これだけだと常に画面中心にしかズームができないので、さらにx,y座標のどこにズームするかを指定できるように以下のように変更してやります。

glm::frustum((left + center_x) / scale, (right + center_x) / scale,
    (bottom + center_y) / scale, (top + center_y) / scale,
    z_near, z_far);

ただ、このコード、正確に動くかは検証していません。
なにせ、私の場合near平面のサイズが既知の条件で扱っているので、以下のようになっているかです。

glm::frustum(
  -0.5f * (this->WindowWidth  + this->Center.x) / this->Scale,
   0.5f * (this->WindowWidth  - this->Center.x) / this->Scale,
  -0.5f * (this->WindowHeight + this->Center.y) / this->Scale,
   0.5f * (this->WindowHeight - this->Center.y) / this->Scale,
  z_near, z_far
);

微妙にthisとかチラチラしてますが基本的な考え方は同じです。

FindPackageで探索してほしくないライブラリを指定する方法

CMakeを使っていて、FindPackageで探索してほしくないパッケージがある時の解決方法。
Anacondaとか入れてると稀に良く困る割に、あんまり紹介されてないなーと思うので、一応メモも兼ねて。

[https://cmake.org/cmake/help/v3.0/variable/CMAKE_DISABLE_FIND_PACKAGE_PackageName.html#variable:CMAKE_DISABLE_FIND_PACKAGE_]



DLIB入れる時にLIBJPEGとLIBPNGについてAnacondaのものを探索してきて困るので以下の2つを足してcmakeしました。

cmake .. -DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON -DCMAKE_DISABLE_FIND_PACKAGE_JPEG=ON

DLIBのコンパイル For Debugビルド

はじめに

今回ビルドするのは、DLIBというC++で書かれた機械学習用のライブラリです。
dlib C++ Library

どこが問題?

Windows環境にて、VisualStudioを使ってDLIBをDebugビルドでコンパイルしようとすると、幾つかの問題に直面します。
中でも気になるのが以下の3つ。

  1. Windowsでは、DLLの生成ができない(__declspec(dllexport)が定義されていない)。
  2. もし、CMakeLists.txt、add_library内のSTATICをSHAREDに置換するとlibpngのEXPORTマクロのおかげでDLLにはlibpngの内容が書き込まれる。
  3. Debugビルドすると以下のリンクエラーが出る。
_USER_ERROR__missing_dlib_all_source_cpp_file__OR__inconsistent_use_of_DEBUG_or_ENABLE_ASSERTS_preprocessor_directives_ 

それで、1、2については諦めました……が3は諦めきれないので何とかします。

解決法

まあ、出てくるエラーを読めば書いているのですが。
ENABLE_ASSSERTSマクロをONにしろと、そういうことだそうです。
以下の様なディレクトリ構造でビルドするときのコマンドを一応載せておきます

dlib
|-- dlib-19.0
`-- build
cmake -DDLIB_ENABLE_ASSERS=TRUE ..
cmake --build . --target INSTALL --config=Debug
cmake --build . --target INSTALL --config=Release

番外編

上のコマンドのまんま実行するとDebugとReleaseでlibファイルの名前が同じなので爆死します。
ということでdlib-19.0\CMakeLists.txtと<インストールされたディレクトリ>\lib\cmake\dlibにあるdlib-.cmakeファイルをそれぞれ編集します。

cmake_minimum_required(VERSION 2.8.4)

IF (CMAKE_SIZEOF_VOID_P EQUAL 8) # Architectureの判定 MACだったらもっとスマートにかけるのになあ…
  SET(__INTERNAL_ARCHITECTURE_STRING__ x64)
ELSE()
  SET(__INTERNAL_ARCHITECTURE_STRING__ x86)
ENDIF()

SET(CMAKE_DEBUG_POSTFIX d_${__INTERNAL_ARCHITECTURE_STRING__})
SET(CMAKE_RELEASE_POSTFIX _${__INTERNAL_ARCHITECTURE_STRING__})

add_subdirectory(dlib)
#----------------------------------------------------------------
# Generated CMake target import file for configuration "Release".
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

IF (CMAKE_SIZEOF_VOID_P EQUAL 8)
  SET(__INTERNAL_ARCHITECTURE_STRING__ x64)
ELSE()
  SET(__INTERNAL_ARCHITECTURE_STRING__ x86)
ENDIF()

# Import target "dlib::dlib" for configuration "Release"
set_property(TARGET dlib::dlib APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(dlib::dlib PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "CXX"
  IMPORTED_LINK_INTERFACE_LIBRARIES_RELEASE "なんか文字列"
  IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/dlib_${__INTERNAL_ARCHITECTURE_STRING__}.lib"
  )

list(APPEND _IMPORT_CHECK_TARGETS dlib::dlib )
list(APPEND _IMPORT_CHECK_FILES_FOR_dlib::dlib "${_IMPORT_PREFIX}/lib/dlib_${__INTERNAL_ARCHITECTURE_STRING__}.lib" )

# Commands beyond this point should not need to know the version.
set(CMAKE_IMPORT_FILE_VERSION)

追記

テストコード貼るの忘れてました。
入力画像からSelectiveSearchで物体っぽい領域を抽出するプログラムです。
本来、閾値処理などで論外な矩形を除外するべきなのですが、それをしていないので、山程矩形が出てきます。
まだ、DLIBの使い方をよく理解していないので、間違っていたらよしなに。

#include <dlib/gui_widgets.h>
#include <dlib/image_io.h>
#include <dlib/image_transforms.h>

#include <string>

#if defined(_DEBUG) || defined(DEBUG)
#pragma comment(lib, R"(dlibd_x86.lib)")
#else
#pragma comment(lib, R"(dlib_x86.lib)")
#endif

int main(int argc, char** argv)
{
  if(argc < 2)
  {
    std::cerr << "Wrong arguments.\n<Exe name> <Input image>" << std::endl;
    return -1;
  }
  char const* INPUT_FILENAME = argv[1];
  dlib::array2d<dlib::rgb_pixel> image, rect_image;
  dlib::load_image(image, INPUT_FILENAME);
  dlib::assign_image(rect_image, image);  // コピーの方法がわかったので修正しました

  std::vector<dlib::rectangle> rectangles;
  dlib::find_candidate_object_locations(image, rectangles);

  std::cout << rectangles.size() << std::endl;

  for (auto&& rect : rectangles)
  {
    dlib::draw_rectangle(rect_image, rect, dlib::rgb_pixel(255, 0, 0), 1);
  }

  dlib::image_window original_window(image, "Original"), find_rectangles(rect_image, "Result");
  
  original_window.wait_until_closed();
  find_rectangles.wait_until_closed();

  return 0;
}

追記2

番外編で書いてることが微妙に嘘だったんで近日中に修正します。
CMAKE_DEBUG_POSTFIXでライブラリファイルの名前を変えた場合にFindPackageで適切なlibファイルを選択させるためには、dlibConfig.cmakeファイルを編集する必要があります。
具体的には38行目周辺のfind_libraryを以下のようにする必要があります。

IF(WIN32)
  find_library(dlib_DEBUG_X86_LIBRARIES dlibd_x86 HINTS ${dlib_INSTALL_PATH}/lib)
  find_library(dlib_DEBUG_X64_LIBRARIES dlibd_x64 HINTS ${dlib_INSTALL_PATH}/lib)
  find_library(dlib_RELEASE_X86_LIBRARIES dlib_x86 HINTS ${dlib_INSTALL_PATH}/lib)
  find_library(dlib_RELEASE_X64_LIBRARIES dlib_x64 HINTS ${dlib_INSTALL_PATH}/lib)
  IF(CMAKE_SIZEOF_VOIDP EQUAL 8)
    set(dlib_RELEASE_LIBRARIES optimized ${dlib_RELEASE_X64_LIBRARIES})
    set(dlib_DEBUG_LIBRARIES debug ${dlib_DEBUG_X64_LIBRARIES})
  ELSE()
    set(dlib_RELEASE_LIBRARIES optimized ${dlib_RELEASE_X86_LIBRARIES})
    set(dlib_DEBUG_LIBRARIES debug ${dlib_DEBUG_X86_LIBRARIES})
  ENDIF()
  set(dlib_LIBRARIES ${dlib_RELEASE_LIBRARIES} ${dlib_DEBUG_LIBRARIES})
ELSE()
  find_library(dlib_LIBRARIES dlib HINTS ${dlib_INSTALL_PATH}/lib)
ENDIF()

SFINAEやテンプレートの部分特殊化で気をつけたいこと

はじめに

久々にSFINAE(Substitution Failure Is Not An Error)やテンプレートの部分特殊化(Partial Specialization)を久々に使ったら見事にハマったので、まとめときます。
ただし、用語の間違えについては自分の中でもあやふやなんで気づいた人は指摘お願いします。

SIFNAEとは

説明が面倒なので以下を参照。

C++ SFINAE - プログラミングの教科書を置いておくところ

環境について

多分、どの環境でも同じだけれども、ボクはVisual Studio 2015 Update2を使ってるんで他の環境ついては知りません。

本題

主に気をつけたいことは以下の3つです。いずれもコンパイルエラーの原因です。

  1. 解決可能なオーバーロードが複数ある。
  2. 実は lvalue もしくは rvalue な参照がテンプレート引数になっている(のに気が付いていない)
  3. 配列を受け取りたいのに何故かエラーになる

どれも解決までに結構悩んだものです。

解決可能なオーバーロードが複数ある

これは最もやらかしやすいと思う。
まあ、コンパイルエラーを読めば、そのまんま書いてあるからその通りに直せば良いのだけれども、量が増えるとそうは行かなくなる。
以下は実例。

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print() { std::cout << "default" << std::endl; }
};

// 本来はポインタ型だけを受け取りたい特殊化
template <class Pointer>
struct type_impl<Pointer, std::enable_if_t<std::is_pointer<Pointer>::value>>
{
  static void print() { std::cout << "Pointer" << std::endl; }
};

// 本来はPOD型を受け取りたい特殊化
template <class POD>
struct type_impl<POD, 
  std::enable_if_t<std::is_pod<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

int main()
{
  type<int*>::print();
  type<int>::print();
  return 0;
}

これ、エラーになります。
見ればわかるんですけど、is_podはポインタを受け取ってもtrueになってしまうからですね。
当然、解決策の検討が必要になるんだけれども、最も安直な方法はこれ。
16行目にポインタを受け取らないようにチェックを足してやる

template <class POD>
struct type_impl<POD, 
  std::enable_if_t<
    std::is_pod<POD>::value && 
    !std::is_pointer<POD>::value  // ポインタは受け取りませんと明示する
  >
>
{
  static void print() { std::cout << "Literal" << std::endl; }
};

実は lvalue もしくは rvalue な参照がテンプレート引数になっている(のに気が付いていない)

以下がその例。っていうかさっきと同じコードのmain関数に1行だけ足した

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print() { std::cout << "default" << std::endl; }
};

template <class Pointer>
struct type_impl<Pointer, std::enable_if_t<std::is_pointer<Pointer>::value>>
{
  static void print() { std::cout << "Pointer" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<POD>::value &&
  !std::is_pointer<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

int main()
{
  type<int*>::print();
  type<int>::print();
  type<int&>::print(); // 足した部分
  
  return 0;
}

さてこれの出力はどうなるでしょうか?
まあ、当たり前なんだけど

Pointer
POD
default

になります。これがどういう時に困るかというと、以下の様な関数を足した時。

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(/* std::forward<Tp>(value) */); // 実際にはvalueを渡すことが多かろう
}

// in int main()
  int hoge = 0;
  print(10);              // 多分 lvalue reference
  print(std::move(hoge)); // 間違いなく rvalue reference
  print(&hoge);           // 間違いなくポインタ
  print(hoge);            // ?

で、これの出力が以下のようになって欲しいのに、

POD
POD
Pointer
POD

以下のようになってしまいます。

POD
default
Pointer
default

一瞬うーんと成るかも知れませんが、参照はPOD型ではありません。
それ故、これを解決するためには remove_reference を使い以下のようにPOD型の判定を行う際に参照を外す必要があります。

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_pointer<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

配列を受け取りたいのに何故かエラーになる

これが一番、厄介、というか一番気づくまでに時間がかかった。
さて、ここで言う配列というのは一次元、それも固定長の配列だ。
この配列だが一般的に以下の方法で配列のサイズを取得することが可能とされている

template <class Array, std::size_t Size>
constexpr std::size_t size(Array(&)[Size])
{
  return Size;
}

int hoge[5] = {};
std::cout << size(hoge) << std::endl; // 5

この方法を応用して以下の様なコードを考える。

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print(Tp&&) { std::cout << "default" << std::endl; }
};

template <class Array, std::size_t Size>
struct type_impl<Array[Size], std::enable_if_t<std::is_array<Array>::value>>
{
  static void print(Array (&& value)[Size]) { std::cout << "Array:(" << value << "," << Size << ")" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_array<POD>::value>
>
{
  static void print(POD&& p) { std::cout << "POD:" << p << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(std::forward<Tp>(value));
}

int main()
{
  int hoge = 20;
  int array[10] = {};
  print(10);
  print(hoge);
  print(array);

  return 0;
}

これを実行すると期待的には以下のとおりにしたいのだが、

POD:10
POD:20
Array:(<arrayのアドレス>,10)

実際は、

POD:10
POD:20
POD:<arrayのアドレス>

となる。
実は、ここで犯しているのはテンプレートの置き換え云々の問題だけではない。
以下がその問題と修正したコードだ。

  • 配列は rvalue referenceで受け取れない。
  • 特殊化するのはArrayだけではなく、Array[Size]。
#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print(Tp&&) { std::cout << "default" << std::endl; }
};

template <class Array, std::size_t Size>
struct type_impl<Array[Size], std::enable_if_t<std::is_array<Array[Size]>::value>>  // 型の渡し方を変える
{
  // Array (&&)[Size] ではなく Array(&)[Size]
  static void print(Array (&value)[Size]) { std::cout << "Array:(" << value << "," << Size << ")" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_array<POD>::value>
>
{
  static void print(POD&& p) { std::cout << "POD:" << p << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(std::forward<Tp>(value));
}

// オーバーロードする
template <class Tp, std::size_t Size, class Traits = type<Tp[Size]>>
void print(Tp (&value)[Size])
{
  Traits::print(value);
}

int main()
{
  int hoge = 20;
  int array[10] = {};
  print(10);
  print(hoge);
  print(array);

  return 0;
}

こうすることで期待通りの結果得られる筈だ。

おわりに

未だにSFINAE機構を使うとやらかすことが多いので、メモを兼ねて記した。
もし、SFINAEやテンプレートの特殊化で困ったことがあれば、この話を思い出して欲しい。

どうでもいいのだけれども

かなりどうでもいい話です。
過去の僕が書いた記事を見ていて、ふと、眼気眼ってなんだと思って。
google検索にかけてみると、なぜか以下のページがトップに出てきた。

ねぼけまなこ【寝惚け眼】の意味 - goo国語辞書

なぜだなぜだ、と見返してみると、眠気眼と書いていたらしい。
実際、Google IMEで「ねむけまなこ」で変換すると眠気眼が出てくる。
が、ここで気になるのが、「ねむけまなこ」と「ねぼけまなこ」の違い。
まあ、結果だけ言うと、「ねむけまなこ」なんて言葉は存在しないらしい。
笑われる

なんとまあ、そんな誤用、変換させるなやとGoogleIMEに八つ当たりしたい所だ。