こんにちは。田原です。

今回は、ちょっと複雑なテンプレートを書く時に便利なdecltypeとstd::declvalです。decltypは既にちらほらつかってますが受け取った式の型を返します。declvalはその逆っぽい機能で受け取った型のインスタンスを返すふりをします。(実際には返さないです。何を言っているか判らないかも?)

1.decltype

1-1.ざっくりの意味

大雑把に説明するとdecltype(式)と書くと式の「型」を返します。

std::cout << TYPENAME(decltype(1+1)) << "\n";  // int

しかし、例外があります。decltype(名前)と書いた場合は、その名前の型を返します。この「名前」は変数名や関数名、クラスのメンバ名です。関数名やメンバ関数名の時、その関数を名前だけで特定出来ない場合(オーバーロードされている時)はエラーになります。

また、式が返却するものは一般には右辺値ですが、C++は左辺値も返却できます
ややこしいのですが、式が返却する左辺値は左辺値参照と右辺値参照の2種類があります。それぞれ、そのまんまですが左辺値参照と右辺値参照を返却します。

1-2.標準規格書の定義

標準規格書(N3337)147ページ「7.1.6.2 Simple type specifiersの4」の定義 は単純ではありません。妙に複雑です。単純に「式の型」ではないのです。

ざっくりですが、私なりに超意訳すると以下の通りです。(分かりやすくするため、かなりデフォルメしてます。意味的には間違ってないつもりですが、何か見落としがあるかも。)

decltype(e)は以下の型を返却する。

  1. eが名前(変数名、関数名、メンバ変数名、メンバ関数名)ならば、eの型
    ただし、関数名だけで関数を確定できない(オーバーロードされている)ならエラー
  2. そうではなくて、eがTの右辺値参照なら、T&&
  3. そうではなくて、eがTの左辺値参照なら、T&
  4. そうではないなら、eの型

配列名を式に組み込むと配列先頭のポインタとなってしまい要素数の情報が抜け落ちてしまいます。なので、上述の「名前」を特別扱いし配列の要素数情報を落とさないで直接型を取り出すという定義にしたのではないかと感じます。
また、関数名の場合も式に組み込むと関数ポインタになりますが、decltypeでは関数へのポインタ型ではなく関数の型が直接返却されます。

#include <iostream>
#include "typename.h"

template<typename tType>
void printType(tType)
{
    std::cout << TYPENAME(tType) << "\n";
}

void foo(int) { }

int main()
{
    int array[]={1,2,3};
    std::cout << "printType(array) : ";
    printType(array);
    std::cout << "decltype(array)  : " << TYPENAME(decltype(array)) << "\n";

    std::cout << "printType(foo) : ";
    printType(foo);
    std::cout << "decltype(foo)  : " << TYPENAME(decltype(foo)) << "\n";
}
printType(array) : int*
decltype(array)  : int [3]
printType(foo) : void (*)(int)
decltype(foo)  : void (int)

2.と3.の左辺値について
規格書ではxvalueならT&&、lvalueならT&と書かれています。xvalueの概念は特に難易度が高いのでこのように記述しました。文法的な間違い(形容詞を名詞で表現している的な)と少し解釈(xvalueは右辺値参照を返す式)が入っています。

2.decltypeの特殊性

2-1.decltypeに関数を渡すだけなら関数の中身はいらない

decltypeの結果は型ですから、例えば次のような記述も可能です。

std::vector<int> aVector;
typedef decltype(aVector.begin()) iteretor;  // ①

もちろん、次のように書いてもよいのですが、std::vector<int>を再度書かないでよいのは有り難いです。

std::vector<int> aVector;
typedef std::vector<int>::iterator iteretor;

そして、上記の①の文でaVector.begin()が呼ばれることはありません。そもそもこの文はコンパイル時に解釈されてiteratorの型を決めるだけですので、実行文として展開されません。
ということは、前回も軽く触れたようにdecltype()の中に書くだけであればstd::vector<int>::begin()の中身がなくても良いのです。

2-2.typeid()との相違に注意

decltypeが返してくる型を確認する際にtypeid()を使って表示すると便利です。
その時、注意点が1つあります。typeid()は型を表示するために用意された機能ではないので、型を表示する時に期待するのとはちょっと異なった振る舞いをします。

typeid(T)が返却する型情報は、Tがconstや参照等の場合、それらを解除してしまいます。
これらの修飾が有ってもなくても「型」の振る舞いが変わるわけではないので不要な情報だから削除するのではないかと思います。(他にvolatileや右辺値参照も同様です。)

しかし、テンプレート・パラメータのそれらは無視しないので正確に表示するためにはテンプレートに与えてから見た方がよいです。

#include <iostream>
#include "typename.h"

template<typename tType>
struct Type { };

int main()
{
    std::cout << "TYPENAME(int)                  = " << TYPENAME(int) << "\n";
    std::cout << "TYPENAME(int const&)           = " << TYPENAME(int const&) << "\n";
    std::cout << "TYPENAME(int volatile&&)       = " << TYPENAME(int volatile&&) << "\n";

    std::cout << "TYPENAME(Type<int>)            = " << TYPENAME(Type<int>) << "\n";
    std::cout << "TYPENAME(Type<int const&>)     = " << TYPENAME(Type<int const&>) << "\n";
    std::cout << "TYPENAME(Type<int volatile&&>) = " << TYPENAME(Type<int volatile&&>) << "\n";
}
TYPENAME(int)                  = int
TYPENAME(int const&)           = int
TYPENAME(int volatile&&)       = int
TYPENAME(Type<int>)            = Type<int>
TYPENAME(Type<int const&>)     = Type<int const&>
TYPENAME(Type<int volatile&&>) = Type<int volatile&&>

Wandboxで確認する。

2-3.decltype(e)とdecltype((e))は異なるので注意

いつもお世話になっている江添氏のブログ「本の虫」にdecltypeの二重括弧が参照になる理由という記事があります。実際書かれている通りなのですが、(e)が参照になるメカニズムをいまいち理解できなくて少し実験してみました。

#include <iostream>
#include "typename.h"

template<typename tType>
struct Type { };

int main()
{
    int e=123;
    std::cout << TYPENAME(Type<decltype(e)>) << "\n";
    std::cout << TYPENAME(Type<decltype((e))>) << "\n";
    std::cout << TYPENAME(Type<decltype((e+e))>) << "\n";
}
Type<int>
Type<int&>
Type<int>

decltype(変数)は変数の型が返ってます。式ではなく直接変数の型が返っていますので「1-2.標準規格書の定義」の 1. に該当します。

decltype((変数))は本の虫記載の通り「変数の参照型」が返ってます。
(変数)は()で括ってしまっているので上述の「名前」ではなく「式」ですから「1-2.標準規格書の定義」の 1. に該当せず、3.の左辺値参照に該当し参照になるということのようです。

最後のdecltype((変数+変数))は「式(変数+変数)」の型intになりました。
名前でもないし、右辺値参照でも左辺値参照でもないので、4. に該当しているようです。

以上のように()で括るだけで参照になる時とならない時の両方があります。ちょっと意外な感じがしますが、判ってしまえばなんとかなりそうですね。

右辺値参照は?
ところで、混乱し易いので省きましがType<decltype(std::move(e))>は右辺値参照(int&&)が返ります。

3.std::declval

これはdecltypeの逆にあたる関数テンプレートです。指定した型が戻り値の型になります。decltypeは式を型へ変換しましたが、std::declvalは型を式(の計算結果のインスタンス)へ変換するのです。

でも、そんなの 「型()」でもできますよね? 例えば「int()」です。これでint型を返す式になります。
だがしかし、作りたい型がクラスでデフォルト・コンストラクタがなかったら、デフォルトでないコンストラクタの引数を指定する必要があります。面倒ですよね。更にそれが同様にデフォルト・コンストラクタのないクラスのインスタンスを与えるものだったりすると気が狂いそうです。

そんな時std::declvalを使うと便利です。実際に指定した型のインスタンスを生成するわけではありませんから、生成するクラスがコンストラクタを持っていなくても生成したふりができます。ですので、テンプレート・プログラミングには便利なのです。

そしてstd::declval<T>()関数を呼び出しては行けません。中身が定義されいるとは限りませんので。
ついさっきでてきたようにdecltypeの中に書いた関数は呼び出されません。ということはdecltypeと組み合わせて使いそうです。(他にも使える場面はあるようです。)
私が知っている使い方としては、前回check()関数をオーバーロードしていますが、そのオーバーロード関数に与える引数を生成する時に使います。

例えば前回のcheck()関数で使う場合は次の通りです。

#include <iostream>
#include <type_traits>
#include <utility>

#define HAS_MEMBER(dMemberName)                     \
    template<class tClass>                          \
    class has_##dMemberName                         \
    {                                               \
        template<class tInnerClass, int dummy=(&tInnerClass::dMemberName, 0)>\
        static std::true_type  check(tInnerClass);  \
        static std::false_type check(...) ;         \
    public :                                        \
        static constexpr bool value = decltype(check(std::declval<tClass>()))::value;\
    }

Wandboxで確認する。

decltypeはないとできてないことが出てくる超ありがたい機能ですが、std::declvalはstd::がついていることから分かるように標準ライブラリによる実装です。つまり自分で書くこともできますので、あると便利な機能ということになります。一々ドキュメント書かなくても「ググれ」の一言で説明できますし、その意味でも便利です。

4.まとめ

今回は主に decltype の解説になってしまいました。decltype はコア言語で実装されていますし、今までできなかった式を型へ変換する超機能ですからちょっと丁寧に解説してみました。
decltypeが無かった前C++11時代はsizeofを使ってテンプレート・プログラミングを頑張っていたようです。ネットのあちこちに残っています。sizeofで式の結果の型のサイズを求めることができるのでdecltypeに近い使い方もできるのです。でもやはり読みにくいので、C++11以降ならばdecltypeを使った方が良いと思います。

さて、次回はメンバ・テンプレートを説明したいと思います。これの明示的特殊化や部分特殊化の書き方はなかなかハードですが、プライマリ・テンプレートであれば特に難しいこともなく通常のテンプレートと同様に使えます。お楽しみに。