C++ (fork) Advent Calendar 2013 : C++ で型推論を活用する

この記事は C++ (fork) Advent Calendar 2013 の 21日目の記事になります。
なんとなく書きたいことがあったので突発的に登録したんですが、周りが割りとガチで戦々恐々としています。
そんな C++ (fork) Advent Calendar 2013 ですが、今回は軽めの記事になります。

[C++03 時代]

今はもう亡き C++03 の時代です。
試しに適当なコードを書いてみます。

[ソース]

#include <string>
#include <iostream>
#include <vector>
#include <typeinfo>
#include <boost/lambda/lambda.hpp>
#include <boost/function.hpp>
#include <boost/range/any_range.hpp>
#include <boost/range/adaptor/sliced.hpp>


// 戻り値型がわからないのでとりあえず T で…
template<typename T, typename U>
T
plus(T t, U u){
    return t + u;
}


// 戻り値型が Seq に依存するので
// any_range を使用
template<typename Seq>
boost::any_range<
    int,
    boost::forward_traversal_tag,
    int,
    std::ptrdiff_t
>
slice1_4(Seq const& seq){
    return seq | boost::adaptors::sliced(1, 4);
}


int
main(){
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);

    // std::vector<int>::iterator を使用する必要がある
    // std::vector 要素の型が変わればここも変える必要がある
    for(std::vector<int>::iterator it = v.begin() ; it != v.end() ; it++){
        std::cout << *it << ",";
    }
    std::cout << "\n";
    std::cout << "--------------------------" << std::endl;

    // slice1_4 の戻り値型は any_range なので
    // 当然 any_range で受け取る必要がある
    boost::any_range<
        int,
        boost::forward_traversal_tag,
        int,
        std::ptrdiff_t
    > result = slice1_4(v);
    // ここで使用したい iterator の型はわからない…。
//     for(??? it = result.begin() ; it != result.end() ; it++){
//         std::cout << *it << ",";
//     }
    std::cout << "\n";
    std::cout << "--------------------------" << std::endl;

    
    namespace lambda = boost::lambda;
    // Boost.Lambda の関数オブジェクトは boost::function で保持する事が可能
    // ただし、シグネチャを定義する必要がある
    boost::function<int(int, int)> twice_int = lambda::_1 + lambda::_1;
    std::cout << twice_int(1, 2) << std::endl;
    
    // その為、シグネチャが異なる場合は再定義する必要がある
    boost::function<float(float, float)> twice_float = lambda::_1 + lambda::_1;
    std::cout << twice_float(-3.25, 1.95) << std::endl;
    std::cout << "--------------------------" << std::endl;

    // plus() の戻り値型は第一引数の型なので順番によって結果が異なる…。
    std::cout << plus(1, 3.14f) << std::endl;
    std::cout << plus(3.14f, 1) << std::endl;

    return 0;
}

[出力]

1,2,3,4,5,
--------------------------

--------------------------
2
-6.5
--------------------------
4
4.14

[wandbox]

http://melpon.org/wandbox/permlink/QGMU4m8G42qYGt3o


さて、上記は C++03 時代には割りとありがちなコードですね。
template を使用すると関数の戻り値型が複雑になる場合が多いです。
そういう時は any_range や function と言った TypeErasure が活用されていたと思います。

[C++11 時代]

しかし、時代はもう C++11 です。
auto を利用して型推論を行うことができます。
上記のコードを C++11 で書きなおしてみましょう。

[ソース]

#include <string>
#include <iostream>
#include <vector>
#include <typeinfo>
#include <boost/lambda/lambda.hpp>
#include <boost/function.hpp>
#include <boost/range/any_range.hpp>
#include <boost/range/adaptor/sliced.hpp>


// t + u の結果を戻り値型にする
template<typename T, typename U>
auto
plus(T t, U u)
->decltype(t + u){
    return t + u;
}


// これも auto と decltype を使用
template<typename Seq>
auto
slice1_4(Seq const& seq)
->decltype(seq | boost::adaptors::sliced(1, 4)){
    return seq | boost::adaptors::sliced(1, 4);
}


int
main(){
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);

    // ローカル変数は auto で定義
    for(auto it = v.begin() ; it != v.end() ; it++){
        std::cout << *it << ",";
    }
    std::cout << "\n";
    std::cout << "--------------------------" << std::endl;

    auto result = slice1_4(v);
    for(auto it = result.begin() ; it != result.end() ; it++){
        std::cout << *it << ",";
    }
    std::cout << "\n";
    std::cout << "--------------------------" << std::endl;

    
    namespace lambda = boost::lambda;

    // 関数のシグネチャを書く必要がないのでどんな型でも受け取る事ができる
    auto twice = lambda::_1 + lambda::_1;
    std::cout << twice(1, 2) << std::endl;
    std::cout << twice(-3.25, 1.95) << std::endl;
    std::cout << "--------------------------" << std::endl;

    // 式の結果を戻り値型にしているので
    // 引数の順番には依存しない
    std::cout << plus(1, 3.14f) << std::endl;
    std::cout << plus(3.14f, 1) << std::endl;

    return 0;
}

[出力]

1,2,3,4,5,
--------------------------
2,3,4,
--------------------------
2
-6.5
--------------------------
4.14
4.14

[wandbox]

http://melpon.org/wandbox/permlink/h0jOoQQvaWImT29m


はい、2つのコードを比べてみるとあきらかにすっきりとして見やすくなっていますね。
auto を使用する事で型を書かずに変数を定義することができます。
特に Boost.Lambda の式を Boost.Funcion を使用することなく保持することができるのが強力ですね。
これで変数の定義時に引数の型を定義する必要がないのでどんな型でも渡す事ができるようになります。


また、auto + decltype を使用して関数の戻り値型も型推論する事が可能です。

// 戻り値型の後方宣言を使用する
// こうすることで t + u の結果の型を戻り値型として定義する事ができる
template<typename T, typename U>
auto
plus(T t, U u)
->decltype(t + u){
    return t + u;
}


ただし、decltype 内で式を定義する必要があるのでコードが重複してしまします。
上記のように短いコードであればさほど気になりませんが、

template<typename Seq>
auto
remove_even(Seq const& seq)
->decltype(seq | boost::adaptors::filtered(boost::lambda::_1 % 2 != 0)){
    return seq | boost::adaptors::filtered(boost::lambda::_1 % 2 != 0);
}


これぐらいになると流石に嫌になってきますね。
ちなみにラムダ式では定義が return 文のみであれば decltype を使用することなく戻り値型の型推論を行ってくれます。

auto make_plus_func  = [](int n){
    return boost::lambda::_1 + boost::lambda::_2 + n;
};
auto plus3 = make_plus_func(3);
std::cout << plus3(1, 2) << std::endl;
// => 6


ラムダベンリヤッター

[C++14 時代]

あれから3年… C++ はパワーアップして返ってきた!
と、いうことで最先端は C++14 らしいです。
C++14 は C++11 の時にもどかしい気持ちでいっぱいだった
関数の戻り値型の型推論
が追加される予定です。
既にいくつかのコンパイラでは実装されているので試してみましょう。

[ソース]

#include <string>
#include <iostream>
#include <vector>
#include <typeinfo>
#include <boost/range/adaptor/sliced.hpp>


// 戻り値型の後方宣言が必要なく
// auto のみで定義
template<typename T, typename U>
auto
plus(T t, U u){
    return t + u;
}

template<typename Seq>
auto
slice1_4(Seq&& seq){
    return seq | boost::adaptors::sliced(1, 4);
}


// 型さえ合っていれば複数の return 文を定義してもよい
auto
is_even(int n){
    if( n % 2 == 0 ){
        return true;
    }
    else{
        return false;
    }
}

// なのでこういう書き方はコンパイルエラー
// auto
// is_odd(int n){
//     if( n % 2 != 0 ){
//         return 1;
//     }
//     else{
//         return false;
//     }
// }

int
main(){
    std::cout << plus(1, 3.14f) << std::endl;
    std::cout << plus(3.14f, 1) << std::endl;

    auto v = std::initializer_list<int>{1, 2, 3, 4, 5};
    auto result = slice1_4(v);
    for(auto&& n : result){
        std::cout << n << std::endl;
    }

    std::cout << is_even(2) << std::endl;
    std::cout << is_even(9) << std::endl;

    return 0;
}

[wandbox]

http://melpon.org/wandbox/permlink/NKz7ZqPwrAkllsv3


こんな感じで戻り値型が auto であれば return 文から自動的に戻り値型の型推論が行われます。
template を使用しているとどうしても戻り値型が大変なことになってしまうケースが多かったのでこれでだいぶ改善されますね。
C++14 はよはよ。

[まとめ]

auto 便利なので C++11(もしくは C++14)を使いましょう。
もうちょっとまじめに書こうと思っていたんですがネタ記事になってしまった…。

[おまけ]

C++14 の仕様を全然把握していないんですけどこんな感じにローカルクラスを関数やラムダ式の戻り値型にするのは仕様的に大丈夫なのだろうか。
特にラムダ式の戻り値型の型推論の条件が緩和されていればだいぶ嬉しいのだけれども。

#include <iostream>
#include <typeinfo>

auto
func(){
    // ローカルで定義した型を返す
    struct X{ int value; };
    return X{10};
}


auto
func2(){
    struct X{ float value; };
    return X{3.14f};
}

int
main(){
    auto x = func();
    auto x2 = func2();
    
    // 別々の型
    static_assert(!std::is_same<decltype(x), decltype(x2)>{}, "");

    // ラムダでも同様に
    auto x3 = []{
        struct X{ int value; };
        return X{42};
    }();

    std::cout << x.value << std::endl;
    std::cout << x2.value << std::endl;
    std::cout << x3.value << std::endl;
    std::cout << typeid(x).name() << std::endl;
    std::cout << typeid(x2).name() << std::endl;
    std::cout << typeid(x3).name() << std::endl;

    return 0;
}

[出力]

10
3.14
42
Z4funcvE1X
Z5func2vE1X
ZZ4mainENK3$_0clEvE1X