Rokiのチラ裏

学生による学習のログ

decltype(auto)

仕事先の休憩スペースにて休憩時間中に投稿。

decltype autoの提案時、僕はC++に触れていなかったため、実装までの過程と脈略が分からず、その必要性がハッキリしなかったのだが、様々な資料を参考に、その必要性を学んだ。そのログをここに残してみる。
結論から言えば、関数の戻り値の型推定機能を強化するためであった。

N3638実装にあたって

N3638:Return type deduction for normal functions
実装内容は、ラムダ式と同じく通常の関数でも戻り値を推定するという内容である。

auto f(){return 1;}

int main()
{
    auto x(f());   // x == 1
            // std::is_integral<decltype(x)>::value == true  
}

このautoはどのように型推定が行われているのだろうか。

autoの型決定の仕組と実引数推定の型変換

autoはテンプレート実引数推定のルールで型の推定が成される。

template<class _Tp>
void f(_Tp);
auto a=1; // decltype(a) == int
f(a); // _Tp == int

しかしこの仕組みは時として予測と反する動きをする事になる。

#include<iostream>
#include<type_traits>

void test_f(){}

template<class _Tp> // _Tp is int*,it is not int[]
bool is_array_on_template(_Tp){return std::is_array<_Tp>::value;}

template<class _Tp> // _Tp is void(*)(),it is not void()
bool is_function_on_template(_Tp){return std::is_function<_Tp>::value;}

template<class _Tp> // _Tp is int,it is not const int
bool is_const_on_template(_Tp){return std::is_const<_Tp>::value;}

int main()
{
    unsigned int array[10]={};
    std::cout<<std::boolalpha<<is_array_on_template(array)<<std::endl; // false
    
    std::cout<<is_function_on_template(test_f)<<std::endl; // false
    
    unsigned int const cobj{};
    std::cout<<is_const_on_template(cobj)<<std::endl; // false
}

autoで表せばこうなる。

// 上コードと型推定における内部処理は同じ
#include<type_traits>
#include<iostream>

void func(){}

int main()
{
    unsigned int array[10]={};
    auto x(array);
    std::cout<<std::boolalpha;
    std::cout<<std::is_array<decltype(x)>::value<<std::endl; // false
    // x is int *. It is not int[].
    
    auto y(func);
    std::cout<<std::is_function<decltype(y)>::value<<std::endl; // false
    // y is void (*)(). It is not void ().

    unsigned int const cobj{};
    auto z(cobj);
    std::cout<<std::is_const<decltype(z)>::value<<std::endl; // false
    // z is int. It is not const int.
}

...とこのように、型の変換が入ってしまう。

参照にも同じ事が言える。

#include<type_traits>
#include<iostream>

int main()
{
    int a(0);
    int& ref(a);

    auto x(ref);
    std::cout<<std::boolalpha<<std::is_lvalue_reference<decltype(x)>::value<<std::endl; // false
    
    auto& z(ref);
    std::cout<<std::is_lvalue_reference<decltype(z)>::value<<std::endl; // true
}

12行目のように、明示的に参照である事を書き込めば、当然意図したように動く。 しかしrvalueになるとそうはいかない。

#include<type_traits>

int main()
{
    int a{};
    int& l=a;
    auto&& x=l;
    static_assert(std::is_rvalue_reference<decltype(x)>::value); // false
    // decltype(x) == int&

    int&& r();
    auto&& y=r();
    static_assert(std::is_rvalue_reference<decltype(y)>::value); // true
    // decltype(y) == int&&

    /* ↑ 初期化子はint&のためxの型はlvalue referenceとなる。
       またint&&は初期化子がrvalue referenceのためyもrvalue referenceとなる。  */
}

autoによる実引数推定の型変換を防ぐdecltype(auto)

このようなテンプレート実引数推定による型変換を行わせないために、decltypeを用いる。

#include<type_traits>
#include<iostream>

void func(){}

int main()
{
    unsigned int array[10]={};
    decltype(array) x;
    std::cout<<std::boolalpha<<std::is_array<decltype(x)>::value<<std::endl; // true
    
    decltype(func) y;
    std::cout<<std::is_function<decltype(y)>::value<<std::endl; // true

    unsigned int const cobj{};
    decltype(cobj) z=cobj;
    std::cout<<std::is_const<decltype(z)>::value<<std::endl; // true
}
#include<type_traits>

int main()
{
    int&& r();
    decltype(r()) g1=r();
    static_assert(std::is_rvalue_reference<decltype(g1)>::value); // true,decltype(g1) == int&&
}

しかし

decltype(expr) variable = expr;

というような、一つの式の内に同じ変数を重複して書かせるような構文は、処理を変えたい時に同じ変数を機械的に重複して書き換えなければならないのでこれはあまり良く無い。

...という事があって、このトラブルの元を潰すために、decltype(auto)という構文が追加された、という事であった。

#include<type_traits>
#include<iostream>

int main()
{
    unsigned int const cobj{};
    decltype(auto) z=cobj;
    std::cout<<std::boolalpha<<std::is_const<decltype(z)>::value<<std::endl; // true

    int&& r();
    decltype(auto) g1=r();
    static_assert(std::is_rvalue_reference<decltype(g1)>::value); // true
}

因みに、初期化子を括弧で囲った式はdecltype指定子に入れると、lvalue referenceとなる。これは仕様であるとの事。

#include<type_traits>

int main()
{
    int a{};

    decltype(auto) x=a;
    decltype(auto) y=(a);

    static_assert(std::is_integral<decltype(x)>::value);
    static_assert(std::is_lvalue_reference<decltype(y)>::value);
}

...で、decltype(auto)の実装目的である「ラムダ式と同じく通常の関数でも戻り値を推論する」というものは

int & f() ;

auto g() -> decltype(f()) { return f() ; } // 多重な重複表現
auto h() -> decltype(auto) { return f() ; }

という事だ。 この表現が可能になった事により、インスタンス化するまで戻り値の型が定まらない、メタプログラミングオーバーロードなどで型がそれらに依存する関数テンプレートなどが、標準規定的に書けるようになった。

Stackoverflowに似たような質問があり、分かりやすかった:c++ - What are some uses of decltype(auto)? - Stack Overflow

正直、decltype(auto)について理解した段階でも、どういうような経緯で実装されたのかを知らなかったら、式の意味としてはよく理解できない仕様なんじゃないかなあと思った。