こんにちは。田原です。

前回はクラス・テンプレートの部分特殊化で複雑な指定を行うための原理的な話を解説しました。うまく組み合わせることで多少複雑な処理でも記述できます。しかもコンパイル時処理なので超高速(実行時間が0)です。ただサンプルを見ても分かるように記述がひたすら長いです。普通のif文のif()とほぼ同じ記述がtypename std::enable_if<>::typeですから。それを短く記述するのにも有用なエイリアス・テンプレート(クラス・テンプレートに別名を付ける機能)がC++11で追加されました。今回はこの機能について解説します。

1.まずはtypedefから

昔からあるtypedefは、型に別名を付ける機能です。次のtypedef宣言により、Handleと記述してもunsignedの意味になります。

typedef unsigned Handle;
Handle gFileHandle=0;	// unsigned gFileHandle=0;と同じ意味

しかし、このtypedefの構文は関数へのポインタ型や配列型の定義を書きにくいです。

typedef void (*FuncPtr)(int);           // 定義した関数ポインタの型名はどれ?
FuncPtr func;				// それはFuncPtrです。

typedef int ArrayInt10[10];             // 定義した配列の型名はどれ?
ArrayInt10 gArrayInt10; 	        // それはArrayInt10です。

型自体の記述と型名の記述が入り交じるので分かりにくいのです。目が踊ってしまいます。

2.すっきり書けるusing

上述の問題を多少なりと改善する構文がC++11にて導入されました。usingです。

using FuncPtr = void (*)(int);
FuncPtr func;				// intを引数とするvoid関数へのポインタ

using ArraiInt10 = int[10];
ArrayInt10 gArrayInt10; 	        // int型10個の配列

関数へのポインタは相変わらず分かりやすいとは言えませんが、型と型名を分けることができるので見やすいです。
そして、普通に関数を宣言する際の関数名の部分を(*)に置き換えるとその関数を指すポインタ型のできあがりです。(因みに、仮引数自体は書いても書かなくてもよいです。)

もちろん関数ポインタや配列だけでなく普通の型の別名を付けることもできます。
つまり、今までと書き方が異なる typedef という訳です。

#include <iostream>

void getData(int iIndex)
{
    std::cout << "getData(" << iIndex << ");\n";
}
using GetDataPtr = void (*)(int iIndex);
GetDataPtr func;                        // intを引数とするvoid関数へのポインタ

int main()
{
    func = getData;
    func(123);
}

3.usingはテンプレートにできます

さて、この型の別名を定義するusingですが、テンプレートにすることができます。
例えば、以下のようにです。

template<bool dCond, typename tType = void>
using enable_if_t = typename std::enable_if<dCond, tType>::type;

先述のように、std::enable_if<bool, tType>::typeはboolがtrueの時typedef tType typeと定義されています。それにenable_if_tという別名を付けています。
これは元の型typename std::enable_if<bool, tType>::typeにつけた別名=エイリアス(alias)ですので、エイリアス・テンプレート(alias template)と呼ばれます。

なお、エイリアス・テンプレートは部分特殊化や明示的特殊化はできません。クラス・テンプレートに別名を付ける際、クラス・テンプレートの部分特殊化や明示的特殊化と複合動作になるので訳が分からなくなりそうですし、有用なのかどうか良く分かりません。個人的には省かれて良かったと感じます。

4.エイリアス・テンプレートを使って条件記述を短くする

前述のように部分特殊化は、条件記述が長くなりがちです。特に typename が嫌らしいです。
このtypenameは「条件」の記述的には意味がなく、コンパイラを手助けするための記述ですから虚しく感じます。
先のエイリアス・テンプレートenable_if_tにより、次のように記述できます。typename部分も含めて別名にしてますので当たり前ですがtypenameの記述も不要になります。

使用前> typename std::enable_if<条件>::type
使用後> enable_if_t<条件>

まだまだif (条件)に比べると長いですが、それなりに短縮できました。
前回の条件記述をこのenable_if_tを使って少し短くしてみました。

前回の記述
 // 部分特殊化1
 template<typename tType>
 struct Template
 <
     tType*,
     typename std::enable_if
     <
         !(std::is_same<tType, char>::value||std::is_same<tType, char const>::value)
     >::type
 >
 {
     // 略
 }

 // 部分特殊化2
 template<typename tType>
 struct Template
 <
     tType*,
     typename std::enable_if
     <
         std::is_same<tType, char>::value||std::is_same<tType, char const>::value
     >::type
 >
 {
     // 略
 }
 // 部分特殊化1
 template<typename tType>
 struct Template
 <
     tType*,
     enable_if_t<!(std::is_same<tType, char>::value||std::is_same<tType, char const>::value)>
 >
 {
     // 略
 }

 // 部分特殊化2
 template<typename tType>
 struct Template
 <
     tType*,
     enable_if_t<std::is_same<tType, char>::value||std::is_same<tType, char const>::value>
 >
 {
     // 略
 }

Wandboxで確認する。

5.C++14やC++17事情

C++14にて、先程のenable_if_tを含む便利クラス・テンプレートが標準ライブラリに追加されていますので、わざわざ自分で定義する必要はありませんし、当該バージョンに対応したコンパイラが使える場合は定義するべきではありません。

ところで、enable_if_tを作ったようにis_same_tを作りたくなりますが、is_sameはvalueです。これはbool型の定数です。型ではありませんので型の別名を定義するエイリアス・テンプレートを使うことはできません。
しかし、C++14で「変数テンプレート」が導入されており、こちらをこの目的で使うことができます。(このような使い方をするときは「変数テンプレート」というよりは「定数テンプレート」ですね。恐らく、変数テンプレート自体は変数にも使えるから「変数テンプレート」なのだろうと思います。)

template<typename tLhs, typename tRhs>
constexpr bool is_same_v = std::is_same<tLhs, tRhs>::value ;

のような使い方ができます。

C++14ならば、上記のis_same_vを使って次のように書けます。

 // 部分特殊化1
 template<typename tType>
 struct Template
 <
	 tType*,
	 std::enable_if_t<!(is_same_v<tType, char>||is_same_v<tType, char const>)>
 >
 {
	 // 略
 }

 // 部分特殊化2
 template<typename tType>
 struct Template
 <
	 tType*,
	 std::enable_if_t<is_same_v<tType, char>||is_same_v<tType, char const>>
 >
 {
	 // 略
 }

更に、C++17にて先程のis_same_vを含む便利クラス・テンプレートが標準ライブラリに追加されていますので、わざわざ自分で定義する必要はありませんし、当該バージョンに対応したコンパイラが使える場合は定義するべきではありません。
C++17の例をWandboxで確認する。

C++17はDraft International Standard (DIS)が確定し、現在発行待ちのようです。2017年の年末までに発行されると期待されているようです。

なお、gccのコンパイラ・オプションはまだC++17にはなっていません。C++17と決定される前C++1xと呼ばれていましたのでC++1xオプションとなっています。

6.条件記述の短縮問題について

ところで、上記のようにエイリアス・テンプレートや変数テンプレートを使い、std::enable_if を where、std::is_same を equal と定義することで、下記のような短縮も可能です。

// 部分特殊化1
template<typename tType>
struct Template<tType*, where<!(equal<tType, char> || equal<tType, char const>)>>
{
     // 略
}

// 部分特殊化2
template<typename tType>
struct Template<tType*, where<equal<tType, char> || equal<tType, char const>>>
{
    // 略
}

Wandboxで確認する。

字面的にはこちらの方が可読性は良いのではないかと思います。
問題はC++の標準ライブラリで使える関数やクラスの数は凄まじいので、その名前を変えて使うと無駄に混乱します。

上記のように標準ライブラリに別名を付ける場合はプロジェクト・メンバーと良く話し合って合意を得られた場合に限ることをお勧めします。また、OSSプロジェクトのように不特定多数の人がメンテナンスするプロジェクトでは止めておいた方がよいかも知れません。

更に、標準ライブラリには聞き慣れないものや他でも良く聞くものが多数あるので、それが標準ライブラリのものであることを明示するためstd::も省略しない方が良いと感じます。coutやistreamのように特徴的なものは省略しても問題ないとは思うのですが、意外に線引が難しく、更にどこに線を引いたのか覚えておくことも難しいので、私自身は全部std::を付けることにしています。

以上の結果、名前を頑張って短くするよりは改行とインデントを駆使する方に私は走っています。

7.まとめ

最近、C#からC++を簡単に呼び出せるようにする仕組みをTheolizerを利用して開発しています。そのためにC#側シリアライザをTheolizerからシュリンクして開発中です。
その際、クラスやenum型、配列、組み込み型で異なる処理を実装する必要があるのですが、それらを同じ方法で呼び出したいためTheolizerではテンプレートを多量に使っています。テンプレートはコンパイル時に展開されますので実行速度への負荷がないため安心して使えます。
しかし、C#のジェネリクス(C++のテンプレートにあたる機能)は、残念ながらコンパイル時処理ではなく実行時処理なので実行速度へ負荷がかかります。C++のありがたみを実感している今日此の頃です。

さて、次回は関数テンプレートのオーバーロードで複雑な条件を指定する方法を解説します。使用する部品は前回と今回解説したものがほとんどです。これに「NULLを指すポインタ型」std::nullptr_tを追加します。
お楽しみに。