Edited at

【C++14】割と汎用的なstd::tuple活用モジュールの例をいくつか

More than 3 years have passed since last update.


std::tupleってなんぞ

無限個の型を持つデータ構造のいわゆるタプル(そのまま)

動的型付けにはとてもよく見られるデータ構造だが、静的型付けのC++でタプルを扱うのはとても面倒くさい。

再帰的に展開したり、メタなことしたり、そんなことにいちいち補助関数を作っていては時間と労力の無駄である。

特にライブラリアンみたいな人には、汎用的にタプルを扱う技術が必須になる、と思う。

というわけで、私がノリと勢いで作った汎用的なタプル専用の補助関数を紹介してみる。

正直どっかで想定しないコピーなどが発生してそうだけれど、まあ、こういう風にやると割とジェネリックになるよ、くらいに留めて読んでほしい。

サンプルのために用意した関数オブジェクトチルドレンと、テンプレート関数オブジェクトを紹介する。

struct A{

public:
A(int init):value(init){}
int value;
void print(){ std::cout << "I am A, value is " << value << std::endl; }
};

struct B{
public:
B(int init):value(init){}
int value;
void print(){ std::cout << "I am B, value is " << value << std::endl; }
};

struct C{
public:
C(int init):value(init){}
int value;
void print(){ std::cout << "I am C, value is " << value << std::endl; }
};

struct f{
template<typename T>
int operator()(T& object,int result, int n){
object.value = (object.value * n)+result;
object.print();
return object.value;
}

template<typename T>
int operator()(T& object,int n){
object.value *= n;
object.print();
return object.value;
}

};

なんか自己紹介とvalueをプリントするだけの関数オブジェクトだ。

フリー関数は、valueをn倍、またresultも受け取った場合は加算したりするものだ。ついでに型情報を自己紹介する。

さて、こいつら関数オブジェクトは同じprintメソッドを持っている。

オブジェクト同士に言語的の関係性はないが、インターフェースが共通のためダックタイピングを利用することができる。

タプルとかはそういうのが得意らしい。仕方ないので使いづらいこいつを一つ腕試ししてやろう。


前方展開

タプルのサイズをNとして

タプルの0~N-1の要素を取り出し、逐次関数にぶん投げる補助関数。

template<size_t index, size_t end, bool isEnd = index == end>

struct forwardExecute;

template<size_t index, size_t end>
struct forwardExecute<index, end, false>
{
template<typename Tuple, typename F, typename... Args>
static void Execute(Tuple&& tuple, F&& f, Args&&... args)
{
f(std::get<index>(tuple),std::forward<Args>(args)...);
forwardExecute<index+1,end>::Execute(
std::forward<Tuple>(tuple),
std::forward<F>(f),
std::forward<Args>(args)...
);

}
};

template<size_t index, size_t end>
struct forwardExecute<index, end, true>
{
template<typename Tuple, class F, typename... Args>
static void Execute(Tuple&& tuple, F&& f, Args&&... args)
{
//end
}
};

template< std::size_t begin, std::size_t end,typename Tuple, typename F,typename... Args>
void forwardExecuteApply(Tuple&& tuple, F&& f, Args&&... args)
{
forwardExecute<begin, end>::Execute(
std::forward<Tuple>(tuple),
std::forward<F>(f),
std::forward<Args>(args)...
);
}

割とよくあるイディオムだとは思う。

データ構造から関数から引数までテンプレートなため、まぁ汎用的と言える。

使ってる技術はC++11ではメジャーなものだ。

1. 可変長引数テンプレート

2. ムーブセマンティクス

この辺りはたぶんここで説明するより、C++erたちのブログを見た方が早い。

使ってみる。

int main(){

A a(1);
B b(2);
C c(3);

auto tuple = std::make_tuple(a,b,c);

forwardExecuteApply<0,3>(tuple,f(),2);

return 0;
}


実行結果


I am A, value is 2
I am B, value is 4
I am C, value is 6


全部に引数の2がかけられて、forwardに展開されている。


二分展開

template<size_t begin, size_t end, bool isEnd = begin + 1 == end>

struct partExecute;

template<size_t begin, size_t end>
struct partExecute<begin, end, true>
{
template<typename Tuple, typename F, typename... Args>
static void Execute(Tuple && tuple, F&& f, Args&&... args)
{
f(std::get<begin>(tuple),std::forward<Args>(args)...);
}
};

template<size_t begin, size_t end>
struct partExecute<begin, end, false>
{
template<typename Tuple, typename F, typename... Args>
static void Execute(Tuple&& tuple, F&& f, Args&&... args)
{
partExecute<begin, (begin + end) / 2>::Execute
(std::forward<Tuple>(tuple), std::forward<F>(f), std::forward<Args>(args)...);

partExecute<(begin + end) / 2, end>::Execute
(std::forward<Tuple>(tuple), std::forward<F>(f), std::forward<Args>(args)...);
}
};

template<std::size_t begin, std::size_t end,typename Tuple, typename F, typename... Args>
void partExecuteApply(Tuple&& tuple, F&& f, Args&&... args)
{
partExecute<begin, end>::Execute(
std::forward<Tuple>(tuple),
std::forward<F>(f),
std::forward<Args>(args)...
);
}

プログラム停止性問題?

なんかそんな感じの問題が証明されているように、コンパイラはコンパイルが終わることを立証することができないため、コンパイル時再帰に制限をかけている。

C++11では16とか32とかそこらへんだった気がするが、そんなちっこい値だと非常に困るので、二分再帰してやることにする。

こうするとlog2(N)しか再帰しなくなるので、よほど大きくなければだいたい処理できる。

二分再帰をしているところを、適当に並列化すれば速度も向上させることができる。

ただし、それには並列化の条件を満たしている必要がある。

前方展開と特に変わったことはしてない。

実行してみる。

int main(){

A a(1);
B b(2);
C c(3);

auto tuple = std::make_tuple(a,b,c);

partExecuteApply<0,3>(tuple,f(),2);

return 0;
}


実行結果


I am A, value is 2
I am B, value is 4
I am C, value is 6



再帰展開

/* recursive and propagate return value. */

template<std::size_t index>
struct propagationTuple{
template<class Tuple, class F, class R,typename... Args>
static auto Execute(Tuple&& tuple,
F&& f,
R&& result,
Args&&... args){
return propagationTuple<index-1>::Execute(std::forward<Tuple>(tuple),
std::forward<F>(f),
std::forward<F>(f)(std::get<index>(tuple),
std::forward<Args>(args)...,
std::forward<R>(result)
),
std::forward<Args>(args)...
);
}
};

template<>
struct propagationTuple<0>{
template<class Tuple,class F, class R,typename... Args>
static auto Execute(Tuple&& tuple,
F&& f,
R&& result,
Args&&... args)
{
return std::forward<F>(f)(std::get<0>(tuple),std::forward<Args>(args)...,std::forward<R>(result));
}
};

template<std::size_t index,class Tuple, class F, typename... Args>
auto propagationTupleApply(Tuple&& tuple,F&& f, Args&&... args){
return propagationTuple<index-1>::Execute(std::forward<Tuple>(tuple),
std::forward<F>(f),
std::forward<F>(f)(std::get<index>(tuple),std::forward<Args>(args)...),
std::forward<Args>(args)...
);
}

驚愕のクソコードになってしまった....

こんなつもりはなかった....

9/16 テンプレート引数追加、返り値を推論してしまうことで簡略化

使った技術

1. expression型推論(decltype)

2. 関数返り値推論

3. タプルのサイズとったりとか参照外したりみたいなメタ関数

_tがついてるメタ関数はテンプレートエイリアスによって::typeが省略されたものだ。

C++14から追加されている。最the高。

はぁ、私はただ後方展開しつつ、計算結果を伝播していくモジュールを作りたかっただけなのにどうしてこうなった...

ま、まぁこういうコードは大概ブラックボックス化するし、多少はね?

実行してみる。

int main(){

A a(1);
B b(2);
C c(3);

auto tuple = std::make_tuple(a,b,c);

std::cout << backPropagationApply(tuple,f(),2) << std::endl;

return 0;
}


実行結果

I am C, value is 6

I am B, value is 10
I am A, value is 12
12

n倍されかつ、結果が後方伝播し加算されている。

A*2 + B*2 + C*2といった感じ。

まぁ、それっぽく動いてるんじゃなかろうか。

ここまではあくまで実装のサンプルで、これらが万能ということでは決してない。

つまり総合的に何が言いたいかというと、テンプレート特殊化と可変長引数テンプレートあたりをうまく使えば、ジェネリックなタプル活用コードか書けるってこと。

このクソコードを見て吐き気を催してしまった方は是非添削をお願いしたいものだ。

自分自身リファクタリングとインデントを放り投げるくらい面倒になった。反省。

追記: 再帰展開のやつ、ところどころコピーしている。ムーブし忘れた。脳内置換して。


結論

std::tupleはむずかしい。