くれなゐの雑記

例を上げて 自分で手を動かして学習できる入門記事を多めに書いています

C++でprint(array)がしたい ~STL判別機のお勉強~

この記事は, KobeUniv Advent Calender 2015 12日目として書かれたものです. 参考文献をめっちゃ頼りにしました. 勉強になりました. 多分今回のプログラムはC++11で動くと思います. vector や array , map はググったら無限に資料が出てくるのでそちらを参照してください 今回の記事を書く上でやりたいことがなかなかみつからなかったので, こういうタイトルにさせていただきました

Motivation

Pythonとかの言語で, arrayをprint()できるのずるくないですか????
C++でもやりたい と思って, ふとコードを書いてみた次第です.
かなり入門から書いてるので, ぜひぜひC++拒否してる人も目を通してみてはいかがでしょうか.

コンテナ

まず, 前提条件として, C++用の配列を知っておいていただきたいです.
コンテナとは, C++の変数の入れ物… のようなものだと思っておいてください.

今回使ってみたコンテナは, map, array, vectorです. それぞれ入門サイト的なところをみると, 早いのでそちらを見てください.

range-based for と, auto型

C++11からの新機能で, 以下のようにかくと, コンテナの中身をすべて引き出すことができます.

std::vector<int> v;
for(auto a: v){
    std::cout << a << std::endl;
}

auto型は, C++11からの新機能で, 初期化されるタイミングで, その場に適した型になる. for(:)は, けして書き間違えではないです. セミコロンではなく, コロンなので気をつけてください. 今回の場合は, 配列の中身を最初から順番に, aに突っ込んでいく みたいなやつです.

みたいなやつです.

TMP:Template Meta Programming

Template Meta Programmingとは, 書いたソースコードを元に, コンパイル時にソースコードを作って, うまいことやる.
みたいなやつです. 特徴としては, ミスったらえげつないほどコンパイルエラー出てきます.
以下に, 簡単な例を上げておきます.


std::vector < double > と, int型が与えられたら, v[index]の値を返す関数を書いてみましょう. vectorは, 可変長配列で, 配列みたいに使えます. std::vector < double > は, 中身がdouble型の配列 を意味します.

double Get(std::vector<double> v, int index) {return v[index];}

簡単ですね

しかし, double型だけに対応していては, 使い勝手が非常に悪いです. intでもfloatでも, あるいは自分の作った構造体でも対応できるようにしたいです.

そういう時に, templateを使います.

template<class T>
T Get(std::vector<T> v, int index){return v[index];}

vectorの中身のdoubleだったものが, Tに変わりました. これで, このTの部分は何でもいいよってことになります. ただし, Tはすべて同じ型でなければなりません. あとは, こういうふうに使います.

std::vector<double> v;
Get<double>(v);

Getの < double > の部分でTの部分はdoubleですよと明記しています.
実は省略しても, コンパイラさんが勝手に判断してくれます.

std::vector<double> v;
Get(v);

さらに, Templateの特殊化についても知っておきたいです.
Tがある時に限定して, ちょっと別の動作をしてほしいときに使います. では実際に書いてみます. coutはC++版のprintf()のようなもので, これひとつでいろんな型を識別して, 出力してくれます.
フォーマット設定が微妙にめんどくさいのがネックですね

#include <iostream>
#include <vector>

using namespace std;

template<class T>
T Get(std::vector<T> v, int index){return v[index];}

template<>
int Get<int>(std::vector<int> v, int index){
    std::cout << "int型だよ" << std::endl;
    return v[index];
}

int main(void){
    std::vector<double> vd;
    std::vector<int> vi;
    for(int i=0;i<100;++i) {
        vd.push_back(i);
        vi.push_back(i);
    }

    std::cout << vd[3] << std::endl;
    std::cout << Get(vd,3) << std::endl;
    std::cout << Get(vi,1) << std::endl;

    return 0;
}

doubleの要素を持つvector vdと, intの要素を持つvector viを2つ用意しました. それぞれに, 0~100の値を入れていきます. Templateの特殊化をする場合, Tの代わりに, 上記のように好きな型などを入れていくと良いのですが, その際にも, templateはかくひつようがあります. 出力結果は以下のようになります.int型をきちんと識別してますね.

3
3
int型だよ
1

さあ, 本題に入りましょう

print(コンテナ)がしたい

残念ながら, c++には, デフォルトでコンテナの中身を出力する関数は用意されていません. coutも対応してませんし, コンテナのスーパークラスなんぞ無いので, 実装が難しいです.

本記事では, コンテナの簡易検出器を制作, そして, それを用いて配列の中身を表示することが目標となっております.

まずは, Containerクラスを作ります. 中身は無です. ちょっと後で必要です.

struct Container{};

次に, C++の都合上, 関数でこれが実装できないので, 関数オブジェクトというものを使って実装します ぱっと見でわかると思いますが, 構造体を関数っぽく使うテクニックです.

コンテナが渡されたら, 中身を表示していく関数オブジェクトを作ります. isFirstとかありますが, 最後にコロンが来ないように工夫してるだけなので, あまり気にしないでください.
{
v[0], v[1], ..., v[n]
}
となるように出力する仕組みです

template<class T>
struct PrintObj<T>
    void operator()(T value){
        std::cout << std::endl << "{";
        bool isFirst = true;
        for(auto a : value){
            if(!isFirst){
                std::cout << ", ";
            }
            std::cout << a;
            isFirst = false;
        }
        std::cout << "}";
    }
};

これは,

std::vector<int> v;
PrintObj<int>()(v);

のようにして書くのですが(関数オブジェクトのとき, <int>は省略不可です, あとカッコが一個おおいです C++17でカッコが省略できるかもしれないみたいな噂は聞きました)

しかしこの手法では, 型の指定とかするのもめんどくさいし, 一次元配列しか出力できないし, 問題だらけです. ここで, 今回製作したソースコードの全貌をお見せします.

#include <array>
#include <cstdlib>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <type_traits>
#include <vector>
#include <array>

struct Container{};

//template吸収用
template <class>
struct Ignore{
    typedef Container type;
};

//1変数出力用のPrintObj
template <class T, class X=Container >
struct PrintObj{
    void operator()(T value){
        std::cout << value;
    }
};


//配列出力用PrintObj
template<class T>
struct PrintObj<T, typename Ignore<typename T::iterator>::type>{
    void operator()(T value){
        std::cout << std::endl << "{";
        bool isFirst = true;
        for(auto a : value){
            if(!isFirst){
                std::cout << ", ";
            }
            PrintObj<typename T::value_type>()(a);
            isFirst = false;
        }
        std::cout << "}";
    }
};


template<class T>
void Print(T value){
    PrintObj<T>()(value);
}

template<class T, class U>
std::ostream& operator << (std::ostream& os, const std::pair<T, U> p){
    os << "(" << p.first << "," << p.second << ")";
    return os;
}

int main(void){
    std::array<int, 5> a = {1,2,3,4,5};

    Print(a);

     return 0;
}

まず, Ignoreは, templateの中身にかかわらず, typeを定義する構造体です. これだけでは特になんの意味もありません. 中身を無視するという意味のIgnoreです.

さて, PringObjが2こあるわけですが, 片方が, 最後の1変数出力用のPringObj, もう片方が, 配列用のPringObjです. 配列用のPringObjを再帰で呼び出し, 最後に1変数出力用のPrintObjを呼び出すことで, 任意の次元数の配列に対応させてます.

まず, C++は特殊化される前の, PrintObjから検証します(X=Containerとなっているため, 省略すれば自動的にXはContainerになります.)
次に, 特殊化されているケースかどうか判断します. 配列用のPringObjが呼ばれるのですが, 判断する際に, typename T::iteratorの部分を実行するわけですが, ここで, Tの中にiteratorがなければここでエラーになります. (厳密な定義とは異なるのですが, 大体のコンテナにはiteratorが入っているはずなので, ここでコンテナかどうか判断します.) エラーになった場合は, C++コンパイルエラーにならないように頑張る特性SFINAEによって, 1変数出力用のPringObjが呼ばれます.
エラーにならなかった場合は, Ignore::typeはかならずContainerになるので, 特殊化され, 配列側が呼び出されることになります.

こうすることで, 簡易配列検出器を制作することができ, 出力することができます

問題点

この手法だと大変大きな問題があって, 普通の配列(int a[3];)みたいなやつを渡した時に, ポインタを表示してしまいます 疲れたからここまで… どうしようかなぁ

参考文献

qiita.com