C++ Boost

The Boost Tuple Library

タプル(またはn-タプル)は決まった個数の要素からなるコレクションである。 2つ組(pair)、3つ組、4つ組などがタプルである。 プログラミング言語においては、タプルは他のオブジェクトを要素として持つデータオブジェクトである。要素となるオブジェクトは異なる型でありうる。

タプルはいろいろな状況で役に立つ。 例えば、2個以上の値を返す関数を定義するのが楽になる。

ML、PythonまたはHaskellなど、いくつかのプログラミング言語は、タプルの構造が組み込まれているが、不運にもC++はそうではない。 この"不足"を補うため、Boost Tuple Libraryは、タプル構造をテンプレートで実装する。

目次

  1. ライブラリを使う
  2. タプル型
  3. タプルを構築する
  4. タプル要素にアクセスする
  5. コピーコンストラクションとタプルの代入
  6. 関係演算子
  7. 結束子
  8. ストリーム化
  9. パフォーマンス
  10. 移植性
  11. 謝辞
  12. 参考文献

もっと詳しく

上位の機能 (いくつかのメタ関数などを紹介する)

一部の設計/実装上の決定に関する隠れた理論的根拠

ライブラリを使う

ライブラリを使うには:

#include "boost/tuple/tuple.hpp"

比較演算子を使うには:

#include "boost/tuple/tuple_comparison.hpp"

タプルの入出力演算子を使うには、

#include "boost/tuple/tuple_io.hpp"
tuple_io.hpptuple_comparison.hppはどちらもtuple.hppをインクルードする。 全ての定義は名前空間::boost::tuplesにあるが、最もよく使われるいくつかの名前は、using宣言を使って、名前空間::boostに持ち上げられている: tuple, make_tuple, tie, getである。さらに、refcref::boostの直下に定義されている。

タプル型

タプル型はtupleテンプレートのインスタンスである。 テンプレート パラメータはタプルの要素の型を指定する。現在のバージョンは、0-10個の要素を使うことができる。 もし必要なら、要素数の上限は、まあ数十個ぐらいまでなら増やせるだろう。 データ要素はC++の型であればなんでもいい。 voidと通常の関数型は、その型のオブジェクトは存在できないものの、正当なC++の型であることに注意してほしい。 あるタプル型が要素としてそれらを含んでいると、タプル型は存在できるが、その型のオブジェクトはできない。 コピーできない、あるいはデフォルト コンストラクトできない要素型には、その性質よりおのずと生じる制限がある。後の'タプルを構築する'を参照されたい。

例えば、次の定義はそれぞれ正当なタプルのインスタンス化である(A, B, Cは何かしらのユーザ定義型と思っていただきたい):

tuple<int>
tuple<double&, const double&, const double, double*, const double*>
tuple<A, int(*)(char, int), B(A::*)(C&), C>
tuple<std::string, std::pair<A, B> >
tuple<A*, tuple<const A*, const B&, C>, bool, void*>

タプルを構築する

タプルのコンストラクタはタプルの要素を引数としてとる。 あるn-要素タプルのコンストラクタは、0 < k <= nである k個の引数をもって召喚することができる。 例えば:

tuple<int, double>() 
tuple<int, double>(1) 
tuple<int, double>(1, 3.14)

要素に初期値が与えられなかったならば、それはデフォルト値で初期化される(というわけでデフォルトの初期化が可能でなければならぬ)。 例えば、

class X {
  X(); 
public:
  X(std::string);
};

tuple<X,X,X>()                                              // error: no default constructor for X
tuple<X,X,X>(string("Jaba"), string("Daba"), string("Duu")) // ok
特に参照型はデフォルトの初期化ができない:
tuple<double&>()                // error: reference must be 
                                // initialized explicitly

double d = 5; 
tuple<double&>(d)               // ok

tuple<double&>(d+3.14)          // error: cannot initialize 
                                // non-const reference with a temporary

tuple<const double&>(d+3.14)    // ok, but dangerous: 
                                // the element becomes a dangling reference 

コピーできない要素に初期値を与えると、コンパイル時にエラーになる:

class Y { 
  Y(const Y&); 
public:
  Y();
};

char a[10];

tuple<char[10], Y>(a, Y()); // error, neither arrays nor Y can be copied
tuple<char[10], Y>();       // ok
だが次の例は全くokである:
Y y;
tuple<char(&)[10], Y&>(a, y); 
構築できないタプル型ができてしまうこともある。 これは、初期化できない要素が、初期化を必要とする要素より前にあった場合に起こる。 例えば: tuple<char[10], int&>.

おおざっばに言って、タプルの構築は、文脈的に、それぞれの要素の構築の集まりにすぎない。

make_tuple関数

タプルはまた、make_tuple(cf. std::make_pair)ヘルパ関数によっても構築することができる。 これを使うと要素型を明示的に記述しなくてもよくなるので、構築はより簡単になる:

tuple<int, int, double> add_multiply_divide(int a, int b) {
  return make_tuple(a+b, a*b, double(a)/double(b));
}

デフォルトでは、要素型は、型推論によって通常の非参照型とされる。例えば:

void foo(const A& a, B& b) { 
  ...
  make_tuple(a, b);
make_tupleの結果は、タプル型tuple<A, B>になる。

例えば、コピーできない型の要素があるなど、通常の非参照型が望ましくない場合もある。 そんな時は、型推論を抑制し、代わりにconstへの参照または非constへの参照型を用いるよう明示することができる。 それには二つのヘルパ テンプレート関数、refcrefを使う。 望みの型を得るために、どの引数でもこれらの関数でラップしてよい。 constオブジェクトをrefしても、結果のタプルにはconstへの参照が入るので、このメカニズムでconstが勝手に剥ぎ取られるようなことはない(次の5行のコードを見ていただきたい)。 例えば:

A a; B b; const A ca = a;
make_tuple(cref(a), b);      // creates tuple<const A&, B>
make_tuple(ref(a), b);       // creates tuple<A&, B>
make_tuple(ref(a), cref(b)); // creates tuple<A&, const B&>
make_tuple(cref(ca));        // creates tuple<const A&>
make_tuple(ref(ca));         // creates tuple<const A&>

配列がmake_tuple関数の引数に与えられたとき、デフォルトでは型推論によりconstへの参照とされる。crefでラップする必要はない。例えば:

make_tuple("Donald", "Daisy");
このコードはtuple<const char (&)[5], const char (&)[6]>型のオブジェクトを生成する(文字列リテラルはconst charの配列であり、const char* ではない)。 しかし、make_tupleで、非const配列の要素を持つタプルを生成したいときには、refを使わなければならない.

関数ポインタは型推論により通常の非参照型とされ、つまり、通常の関数ポインタになる。 タプルには関数への参照も入れることができる。 しかしこのようなタプルはmake_tupleで構築することができない(結果的にconst修飾された関数型となり、文法違反になるためである):

void f(int i);
  ...
make_tuple(&f); // tuple<void (*)(int)>
  ...
tuple<tuple<void (&)(int)> > a(f) // ok
make_tuple(f);                    // not ok

タプル要素にアクセスする

タプル要素には次の式でアクセスすることができる:

t.get<N>()
または
get<N>(t)
tはタプルオブジェクトであり、Nはアクセスされる要素のインデックスを特定する汎整数定数式である。 tがconstか否かによって、getが、N番目の要素を、constへの参照として返すか、非constへの参照として返すかが決まる。 最初の要素のインデックスは0であり、したがってNは0からk-1(kはタプルの要素の数)でなければならない。 さもなくばコンパイル時にエラーになる。例えば:
double d = 2.7; A a;
tuple<int, double&, const A&> t(1, d, a);
const tuple<int, double&, const A&> ct = t;
  ...
int i = get<0>(t); i = t.get<0>();        // ok
int j = get<0>(ct);                       // ok
get<0>(t) = 5;                            // ok 
get<0>(ct) = 5;                           // error, can't assign to const 
  ...
double e = get<1>(t); // ok   
get<1>(t) = 3.14;     // ok 
get<2>(t) = A();      // error, can't assign to const 
A aa = get<3>(t);     // error: index out of bounds 
  ...
++get<0>(t);  // ok, can be used as any variable
注意! メンバ関数のgetはMS Visual C++ compilerではサポートされていない。 そのうえこのコンパイラは、明示的に名前空間を指定しないと、非メンバ関数のgetを使うときにもトラブルが起こる。そのため、MSVC++ 6.0.でコンパイルされるであろうコードを書くときは、全てのget呼び出しに、このように指定をした方がよい: tuples::get<N>(a_tuple)

コピーコンストラクションとタプルの代入

タプルは、要素ごとのコピーコンストラクトができるなら、他のタプルからコピーコンストラクトすることができる。 同様に、要素ごとの代入ができるなら、他のタプルに代入することができる。 例えば:

class A {};
class B : public A {};
struct C { C(); C(const B&); };
struct D { operator C() const; };
tuple<char, B*, B, D> t;
  ...
tuple<int, A*, C, C> a(t); // ok 
a = t;                     // ok 
どちらの場合でも、変換は次のように行われる: char -> int, B* -> A* (派生クラスへのポインタから基本クラスのポインタへ), B -> C (ユーザ定義の変換), D -> C (ユーザ定義の変換)。

std::pairからの代入も定義されている:

tuple<float, int> a = std::make_pair(1, 'a');

関係演算子

タプルは==, !=, <, >, <=, >=演算子を、対応する、要素ごとの演算子に還元する。 これは、いずれかの演算子が、二つのタプルの全ての要素に対して定義されていれば、その演算子が、タプルについても定義されることを意味する。 二つのタプルabに対する等価演算子は、このように定義される:

<, >, <=, >=演算子は、辞書式順序を実装する。

要素数の異なる二つのタプルを比較しようとする試みは、コンパイル時エラーになる。

また、比較演算子は"短絡的"である: 要素ごとの比較は最初の要素から順に始められるが、結果が明らかになるまでしか行われない。

例:

tuple<std::string, int, A> t1(std::string("same?"), 2, A());
tuple<std::string, long, A> t2(std::string("same?"), 2, A());
tuple<std::string, long, A> t3(std::string("different"), 3, A());

bool operator==(A, A) { std::cout << "All the same to me..."; return true; }

t1 == t2; 		// true
t1 == t3;               // false, does not print "All the..."

結束子

結束子はタプルの一種で、全ての要素が非constへの参照型のものである。 これらはtie関数テンプレート(cf. make_tuple)を召喚することによって構築される:

int i; char c; double d; 
  ...
tie(i, c, a);

上のtie関数は、tuple<int&, char&, double&>型のタプルを生成する。 make_tuple(ref(i), ref(c), ref(a))によっても同じ結果が得られる。

要素として非constへの参照を持つタプルは、別のタプルをいくつかの変数に'荷解き'するために利用することができる。例えば:

int i; char c; double d; 
tie(i, c, d) = make_tuple(1,'a', 5.5);
std::cout << i << " " <<  c << " " << d;
このコードは標準出力ストリームに1 a 5.5とプリントする。 このようなタプル荷解き操作は、MLとPythonの例に見ることができる。 タプルを返す関数を呼ぶときに役立つ。

tieのメカニズムはstd::pairテンプレートに対しても同様に働く:

int i; char c;
tie(i, c) = std::make_pair(1, 'a');

ignore

ignoreは、タプルから代入された要素を無視することを可能にするオブジェクトである。 タプルを返す関数の、返却値の一部にしか興味が無いときに使う。例えば(ignoreは入れ子の名前空間tuplesにある):
char c;
tie(tuples::ignore, c) = std::make_pair(1, 'a');

ストリーム化

グローバルのoperator<<は、タプルをstd::ostreamに出力するとき、それぞれの要素について再帰的にoperator<<を呼び出すよう、オーバーロードされている。

同様に、グローバルのoperator>>も、タプルをstd::istreamから抽出するとき、それぞれの要素について再帰的にoperator>>を呼び出すよう、オーバーロードされている。

デフォルトの区切り文字は空白で、タプルの前後は括弧でくくられる。 例えば:

tuple<float, int, std::string> a(1.0f, 2, std::string("Howdy folks!");

cout << a; 
このコードはタプルをこのように出力する: (1.0 2 Howdy folks!)

ライブラリは、デフォルトの挙動を変えるために、3つのマニピュレータを定義している:

これらのマニピュレータは、入れ子の名前空間tuplesに定義されている。 例えば:
cout << tuples::set_open('[') << tuples::set_close(']') << tuples::set_delimiter(',') << a; 
このコードは同じタプルをこのように出力する: [1.0,2,Howdy folks!]

同じマニピュレータはoperator>>istreamにも同じように働く。cinストリームに次のようなデータがあるとしよう:

(1 2 3) [4:5]
次のコード:
tuple<int, int, int> i;
tuple<int, int> j;

cin >> i;
cin >> tuples::set_open('[') >> tuples::set_close(']') >> tules::set_delimiter(':');
cin >> j;
はタプルijにデータを読み込む。

std::stringやCスタイルの文字列を含むタプルを抽出するのは、一般的にはうまくいかない。ストリーム化されたタプルの表現は、あいまいでない構文解析ができるとは限らないからである。

パフォーマンス

タプルの全てのアクセス関数や構築関数は、小さなインライン化された一行関数である。 したがって、 標準的な処理系なら、タプルを使うことで、手書きのタプルに似たクラスよりかかる余分なコストを除去できるだろう。 具体的には、そういう処理系なら、このコードと:
class hand_made_tuple { 
  A a; B b; C c;
public:
  hand_made_tuple(const A& aa, const B& bb, const C& cc) 
    : a(aa), b(bb), c(cc) {};
  A& getA() { return a; };
  B& getB() { return b; };
  C& getC() { return c; };
};

hand_made_tuple hmt(A(), B(), C()); 
hmt.getA(); hmt.getB(); hmt.getC();
このコードに:
tuple<A, B, C> t(A(), B(), C());
t.get<0>(); t.get<1>(); t.get<2>(); 
パフォーマンスの差は無い。

広く使われている処理系でも、タプルのこの種の用途について最適化をしくじるものが存在する(例えば bcc 5.5.1)ことに注意されたい。

処理系の最適化能力に依存するが、tieのメカニズムは、関数が複数の値を返すために非const参照引数を使うのに比べて、小さなペナルティしかもたらさないだろう。 例えば、次の関数f1f2が、等価な機能を持っているとしよう:

void f1(int&, double&);
tuple<int, double> f2();
この場合、次のコードの関数呼び出し#1は、#2よりわずかに早いだろう。
int i; double d;
  ...
f1(i,d);         // #1
tie(i,d) = f2(); // #2
より突っ込んだ議論は、 [1, 2] を参照されたい。

コンパイル時間に与える影響

タプルをコンパイルする時、テンプレートのインスタンス化を大量に行う必要があるので、遅くなることがある。 処理系やタプルの要素数に依存するが、タプルの構築は、前に出てきたhand_made_tupleのような、明示的に書かれた等価なクラスをコンパイルするのに比べ、10倍以上遅くなるだろう。 しかし、現実的には、プログラムは、タプルの定義以外にも多量のコードを含んでいるであろうから、その差はたぶん、気になるほどではないだろう。 非常に頻繁にタプルを使用した、複数のプログラムで計測した結果、コンパイル時間は5から10パーセント増大した。 同じテストプログラムで、コンパイル時のメモリ消費量は22から27パーセント増大した。 詳しくは [1, 2] を参照されたい。

移植性

ライブラリのコードは標準C++である(?)ので、標準規格に合致した処理系で使うことができる。 以下に、処理系の一覧と、それぞれの処理系についての既知の問題を掲げる:

処理系問題
gcc 2.95-
edg 2.44-
Borland 5.5関数ポインタとメンバ ポインタをタプルの要素にできない
Metrowerks 6.2refcrefラッパが使えない
MS Visual C++参照を要素にできない(でもtieは動作する)。refcrefラッパが使えない

謝辞

Gary Powell has been an indispensable helping hand. In particular, stream manipulators for tuples were his idea. Doug Gregor came up with a working version for MSVC. Thanks to Jeremy Siek, William Kempf and Jens Maurer for their help and suggestions. The comments by Vesa Karvonen, John Max Skaller, Ed Brey, Beman Dawes, David Abrahams and Hartmut Kaiser helped to improve the library. The idea for the tie mechanism came from an old usenet article by Ian McCulloch, where he proposed something similar for std::pairs.

参考文献

[1] Järvi J.: Tuples and multiple return values in C++, TUCS Technical Report No 249, 1999 (http://www.tucs.fi/publications).

[2] Järvi J.: ML-Style Tuple Assignment in Standard C++ - Extending the Multiple Return Value Formalism, TUCS Technical Report No 267, 1999 (http://www.tucs.fi/publications).

[3] Järvi J.:Tuple Types and Multiple Return Values, C/C++ Users Journal, August 2001.


Last modified 2001-09-13

© Copyright Jaakko Järvi 2001. Permission to copy, use, modify, sell and distribute this software and its documentation is granted provided this copyright notice appears in all copies. This software and its documentation is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose.

Japanese Translation Copyright (C) 2003 Yoshinori Tagawa. オリジナルの、及びこの著作権表示が全ての複製の中に現れる限り、この文書の 複製、利用、変更、販売そして配布を認める。このドキュメントは「あるがまま」 に提供されており、いかなる明示的、暗黙的保証も行わない。また、 いかなる目的に対しても、その利用が適していることを関知しない。