こんにちは。田原です。
クラス・テンプレートの部分特殊化や関数テンプレートのオーバーロードはある程度範囲の型に対してテンプレートを定義します。その「範囲」をかなり自由に指定することができます。今回はそれによく使われる幾つかの部品と、範囲指定するためにたいへん有用なSFINAEの利用方法について解説します。
1.constexprでコンパイル時定数を定義する
C++11でconstexprキーワードが追加されました。これは部分特殊化やオーバーロードを積極的に使う場面でもよく使われます。他の使い方もありますが、今回は部分特殊化やオーバーロードで使われる比較的簡単な使い方について説明します。
まず、constexprは、「constant expression (定数式)」の略語だそうです。
そして、constexprは、グローバル変数、ローカル変数、staticメンバ変数の初期化付き宣言に指定できます。つまり、指定された変数が「定数」であることを指定します。
入門編で解説したconstも似たようなことができます。こちらは「constant(定数)」の略語です。定数と定数式、単語だけでは何が違うか良く分かりませんね。実際、constとほぼ同じ使い方ができる場面もあります。実は今から解説する使い方の場合は大差ありません。
#include <iostream>
constexpr int x=10;
const int y=20;
int array0[x];
int array1[y];
int main()
{
std::cout << "x=" << x << "\n";
std::cout << "y=" << y << "\n";
}
x=10 y=20
xもyも定数となり、グローバル配列宣言の要素数として使えますので、どちらもコンパイル時に値が定まる定数です。
他にローカル変数、staticメンバ変数で上記のように素の型に対して使う場合のみconstとconstexprは同じ振る舞いをします。これ以外のケースではconstとconstexprは別ものです。指定できる対象や振る舞いが異なります。
constexprは変数を修飾し、修飾された変数が「コンパイル時定数」であることを指定します。
これに対し、constは型を修飾し、修飾された部分が「リードオンリ」であることを指定します。
最大の差は、constは初期化文の実行時に値が決まっていれば十分なことに対して、constexprはコンパイル時に値が決まっている必要がある点です。
#include <iostream>
int a=10;
const int b=a; // constは変数で初期化可能
//constexpr int c=a; // constexprは変数で初期化できない
int main()
{
std::cout << "a=" << a << "\n";
std::cout << "b=" << b << "\n";
// ローカル変数のアドレスで初期化
int d=20;
const int* x=&d; // この文の実行時cのアドレスは決まっている
// constexpr int* y=&d; // コンパイル時にcのアドレスは決まっていない
std::cout << "*x=" << *x << "\n";
}
a=10 b=10 *x=20
グローバル変数は例え初期化されていてもコンパイル時に値が決まっていないケースもあります。他のコンパイル単位で定義されていてexternでアクセスしているような時は現在のコンパイル単位内では値が決まりません。そのためと思いますが、一律に変数では初期化できないことになっているようです。
さて、修飾先の違いにより、次のようなちょっと不思議な現象がおきます。
(ポインタのconst修飾については第32回目の2-2.ポインタ周りのconst修飾 First Levelを参照下さい。)
#include <iostream>
int a=10;
int b=20;
int main()
{
const int* x=&a;
constexpr int* y=&a;
x = &b; // x自身は「定数」ではない
// y = &b; // y自身が「定数」である
// *x = 30; // xの指す先が「定数」である
*y = 30; // yの指す先は「定数」ではない
std::cout << "*x=" << *x << "\n";
std::cout << "*y=" << *y << "\n";
}
(グローバル変数のアドレスは厳密にはリンク時かアプリケーション・プログラムの起動時に決まりますが、例外的にアドレスでポインタを初期化することは許可されているようです。)
*x=20 *y=30
コンパイルする時に値が決まっていると決めた変数にはconstではなくconstexprを指定しておくと、コンパイル時に決まらない可能性(恐らくバグ)がある場合、コンパイラがエラーにしてくれますので、バグの発見が速くなります。
そして、テンプレートはコンパイル時に実体化(マシン語コードが出力)されます。ですので、部分的特殊化されたテンプレートのどれを実体するのかコンパイラがコンパイル時に決定できる必要があります。従って、部分特殊化の「範囲」を定める際に「定数」を用いる場合、それは「コンパイル時定数」でないといけませんから、constexprで指定する場合が多いです。
#include <iostream>
// 型から取り出す
template<class tAnimal>
int getLegs()
{
return tAnimal::legs;
}
// クラスのインスタンスから取り出す
template<class tAnimal>
int getLegs(tAnimal const&)
{
return tAnimal::legs;
}
// 犬
struct Dog
{
constexpr static int legs = 4;
};
// 猫
struct Cat
{
constexpr static int legs = 4;
};
// カブトムシ
struct Beetle
{
constexpr static int legs = 6;
};
// タコ
struct Octopus
{
constexpr static int legs = 8;
};
// イカ
struct Squid
{
constexpr static int legs = 10;
};
int main()
{
// 型から取り出したい時
std::cout << "Dog =" << getLegs<Dog>() << "\n";
std::cout << "Cat =" << getLegs<Cat>() << "\n";
std::cout << "Beetle =" << getLegs<Beetle>() << "\n";
std::cout << "Octopus=" << getLegs<Octopus>() << "\n";
std::cout << "Squid =" << getLegs<Squid>() << "\n";
// インスタンスから取り出したい時
Dog aDog;
std::cout << "aDog =" << getLegs(aDog) << "\n";
}
Dog =4 Cat =4 Beetle =6 Octopus=8 Squid =10 aDog =4
constexpr変数の型は「リテラル型」であることが必要です。通常のint型等の基本型はリテラル型です。更にクラスの一部もリテラル型です。
クラスがリテラル型かどうかの定義はなかなかハードです。また、変数以外に関数と関数テンプレートにもconstexprを指定できますが、やはり難易度が高いです。実は これらの難しい部分は私の手に余ってまして、その有用性を十分には理解できていません。ですので、申し訳ないですが今のところ割愛致します。
2.typenameで「型」であることをコンパイラに教える
テンプレート・パラメータにクラスを渡して、そのクラスが定義する型を取り出したい時があります。非常に単純化していますが、以下のようなイメージです。
template<typename tType>
struct Foo
{
typedef tType type;
};
このFoo<型>をテンプレートに渡して、そのテンプレートの中でtypeを使いたい時、typename指定が必要になります。
#include <iostream>
#include "typename.h"
template<class tClass>
void print()
{
std::cout << "name=" << tClass::name << "\n";
std::cout << "type=" << TYPENAME(typename tClass::type) << "\n";
}
Wandboxで確認する。 typenameを削除してみて下さい。
print()はテンプレートですので、tClassに型を指定して実体化するまでtClassの中身が分かりません。
そのため、コンパイラがprint()テンプレートを最初に解析する時にはtClass::nameやtClass:typeが型なのか、そうでないのか分からりません。それではコンパイラが困るためnameやtypeが型の場合はそれをプログラマがコンパイラへ教えることになっています。「型」の時にtypenameを指定します。そうでない時はtypenameを書きません。(普通のstaticメンバ・アクセスと同じです。)
もし、TYPENAME(typename tClass::type)をTYPENAME(tClass::type)と記述した場合、tClass::typeは型ではないと解釈され、TYPENAME()マクロの引数が型ではないものとして解釈されます。しかし、実体化時に渡しているものは型ですので、事前処理と実体化処理に矛盾が生じエラーになります。
なお、tClassではなく実際の型(上記WandboxサンプルではFoo<int>)を指定している時は当然typeが型であることが分かりますからtypenameの指定は不要です。
つまり、テンプレート内でテンプレート引数に依存した型を取り出す時はtypename指定が必要になります。
3.非型パラメータ
今までテンプレート・パラメータには、型パラメータだけのサンプルを挙げてました。しかし、例えばint型の値(1や2や1453などなど)を指定する非型パラメータと呼ばれるパラメータも定義することができます。
例えば、次のような使い方をします。
#include <iostream>
template<int kInt>
struct Int
{
constexpr static int value = kInt;
};
int main()
{
typedef Int<123> Int123;
std::cout << "Int123::value=" << Int123::value << "\n";
}
Int123::value=123
更に、テンプレートは型パラメータを受け取れますが、その型パラメータで指定した型の値を受け取ることができます。
#include <iostream>
template<typename tType, tType kValue>
struct Any
{
constexpr static tType value = kValue;
};
int main()
{
typedef Any<int, 123> Int123;
std::cout << "Int123::value=" << Int123::value << "\n";
typedef Any<bool, false> BoolFalse;
std::cout << "BoolFalse::value=" << BoolFalse::value << "\n";
}
非型パラメータとしてよく使うものは、整数型とenum型、std::nullptr_t型です。
また、非型パラメータにクラスのインスタンスや浮動小数点値を直接指定することはできません。
一度グローバル変数として定義してその参照やポインタとすれば指定できますが、使うことは少ないと思います。使い方も難しいので割愛します。
4.デフォルト・テンプレート・パラメータ
関数の仮引数にデフォルト値を指定することができました。テンプレート・パラメータもデフォルト型やデフォルト値を指定することができます。
#include <iostream>
#include "typename.h"
template<typename tType, typename tType2=int>
struct Foo
{
typedef tType type;
typedef tType2 type2;
};
template<typename tType, int kInt=123>
struct Bar
{
typedef tType type;
constexpr static int value = kInt;
};
int main()
{
typedef Foo<double> FooDouble;
std::cout << "FooDouble::type2=" << TYPENAME(FooDouble::type2) << "\n";
typedef Foo<short, unsigned> FooShortUnsigned;
std::cout << "FooShortUnsigned::type2=" << TYPENAME(FooShortUnsigned::type2) << "\n";
typedef Bar<float> BarFloat;
std::cout << "BarFloat::value=" << BarFloat::value << "\n";
typedef Bar<float, 567> BarFloat567;
std::cout << "BarFloat567::value=" << BarFloat567::value << "\n";
}
5.そしてSFINAE(Substitution Failure Is Not An Error)
クラス・テンプレートに実引数を指定して実体化する時、テンプレート定義(プライマリー、部分的特殊化、明示的特殊化)のどれを用いるのか決定しますが、その際に、テンプレート仮引数に実引数を仮に当てはめた結果エラーが起きてもエラーにせず、単にその部分特殊化や明示的特殊化を選択しないという仕組みのことをSFINAEと呼びます。
サンプルです。
#include <iostream>
template<int>
struct Helper
{
typedef void type;
};
template<class tClass, typename tEnable=void>
struct IsFish
{
constexpr static bool value = false;
};
template<class tClass>
struct IsFish<tClass, typename Helper<tClass::filets>::type>
{
constexpr static bool value = true;
};
// 犬
struct Dog
{
constexpr static int legs = 4;
};
// 鮭
struct Salmon
{
constexpr static int filets = 8;
};
int main()
{
std::cout << "Is dog a fish ? " << IsFish<Dog>::value << "\n";
std::cout << "Is salmon a fish ? " << IsFish<Salmon>::value << "\n";
}
犬は足(legs)を持ち、鮭はヒレ(filets)を持ちます。そこで、このサンプルはヒレがあったら魚(IsFish)であると判定しています。原理は今回説明した内容の組み合わせでできています。
まず、Helperクラス・テンプレートはint型の非型パラメータを受け取りますが、それが何であろうとtypeをvoid型で定義します。
IsFishクラス・テンプレートの部分的特殊化は、指定されたtClassのfiletsをHelperクラスのテンプレート・パラメータとして与えています。tClassがfiletsを持っていたらHelper<tClass::filets>はエラーになりませんので、このtypeはvoid型になります。つまり、この部分的特殊化はIsFish<tClass, void>に展開されますが、voidは使っていないので実はどうでもよいです。
ポイントはHelper<tClass::filets>がエラーになるかならないかです。
tClassにDogを指定した場合、Dogはfiletsを持っていないためエラーになります。エラーになるのですが、ここは実体化するテンプレート定義を探しているところですから、SFINAEの働きでエラーにならず単にその部分的特殊化を選択しないだけです。その結果、プライマリー・テンプレートが選択され無事valueがfalseになるのです。
6.まとめ
今回は、部分的特殊化で「型」の範囲を複雑な式で指定するための基本的な部品について解説しました。
constexprについてはちょっとページを裂きすぎたかも知れません。constでもできる部分ですので実はそれほど重要ではないからです。(5.のサンプルはconstでも同じように動作します。constならC++11より古い規格でも動作します。)
でも、言語的な仕組みとしてはconstexprの方が素直な使い方になると思います。constexprが無かった頃にちょっと無理してconstを割り当てている印象だからです。constはリードオンリであってコンパイル時定数ではないと考えた方が全体にスッキリしますので。
さて、今回はSIFNAEによる高度な部分的特殊化の基礎を説明しました。次回は、この周辺にある便利な標準ライブラリを含めた解説をしたいと思います。ところで、実は今、ちょっと後悔してます。テンプレート周辺はC++14以降で結構便利になってます。応用編はC++14対応した方が良かったかも。ですので、少しC++14の話題を混ぜながら進めたいと思います。
それではまた来週。
