Boost C++: библиотека Tuple - кортежи

Тип tuple (или n-tuple) это коллекция элементов фиксированного размера (см. другие определения). Пары (pairs), тройки (triples), четверки (quadruples) и так далее все являются объектами типа tuples. В языке программирования tuple это объект данных, содержащий другие объекты как элементы. Эти элементы могут быть разных типов.

Использование типа tuple удобно в разных условиях. К примеру, тип tuple облегчает создание функции, которая возвращает более чем одно значение.

Некоторые языки программирования, такие как ML, Python и Haskell, имеют встроенную поддержку для tuple. К сожалению язык C++ не имеет такой поддержки. Библиотека Boost.Tuple содержит реализацию типа tuple посредством шаблонов.

Примечание переводчика: при переводе слова tuple мы использовали достаточно устоявшийся термин кортеж вместо корявой кальки тупл.

Содержимое

Использование библиотеки

Типы tuple'ов

Конструирование объектов tuple

Доступ к элементам объектов tuple

Создание копированием и присваивание tuple

Операторы сравнения

Связки (tiers)

Операции с потоками

Эффективность

Портабельность

Признательности

Ссылки

Дополнительные детали

Расширенные возможности (описаны некоторые метафункции и др.).

Объяснение для некоторых решений в реализации и дизайне.

Использование библиотеки

Для использования библиотеки просто включите строку:

#include "boost/tuple/tuple.hpp"

Операторы сравнения могут быть также подключены так:

#include "boost/tuple/tuple_comparison.hpp"

Для использования операторов ввода/вывода для кортежей,

#include "boost/tuple/tuple_io.hpp"

Хидеры tuple_io.hpp и tuple_comparison.hpp оба включают tuple.hpp.

Все объявления помещаются в пространстве имен ::boost::tuples, но наиболее употребительные имена подняты также в пространство имен ::boost. Эти относится к следующим именам: tuple, make_tuple, tie и get. Далее, ref и cref прямо объявлены в пространстве имен ::boost.

Типы кортежей

Кортеж получается конкретизацией шаблона tuple. Параметры этого шаблона задают типы хранимых элементов. Текущая версия поддерживает кортежи, содержащие от 0 до 10 элементов. Если необходимо, верхний предел может быть увеличен до, скажем, нескольких десятков. Хранимый элемент может иметь любой тип C++. Обратите внимание, что void и обычные функции являются корректными типами C++, но объекты этих типов не могут существовать. Следовательно, если кортеж содержит такие типы элементов, то объект типа tuple может существовать, но объекты соответствующих типов элементов - нет. Есть естественные ограничения на типы элементов, которые не могут быть скопированы, или не могут быть сконструированы по умолчанию (см. далее 'Конструирование tuple').

К примеру, следующие объявления являются корректными (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*>

Конструирование кортежей

Конструктор tuple получает элементы кортежа как аргументы. Для n-элементных коотежей можно вызывать конструктор с числом аргументов k, где 0 <= k <= n. К примеру:

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

Если для элемента не задано начального значения, то он инициализируется по умолчанию (и, следовательно, должен допускать такую инициализацию). К примеру.

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

tuple<X,X,X>()                                              // ошибка: нет конструктора по умолчанию X
tuple<X,X,X>(string("Jaba"), string("Daba"), string("Duu")) // ok

В частности, ссылочные типы не могут быть инициализированы по умолчанию:

tuple<double&>()                // ошибка: ссылка должна быть инициализирована явно 

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

tuple<double&>(d+3.14)          // ошибка: нельзя инициализировать неконстантную ссылку временным объектом 

tuple<const double&>(d+3.14)    // ok, но опасно: 
                                // элемент становится повисшей ссылкой 

При использовании начального значения для величины, которая не поддерживает копирование, вызывает ошибку компиляции:

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

char a[10];

tuple<char[10], Y>(a, Y()); // ошибка, ни массив, ни Y не могут быть скопированы
tuple<char[10], Y>();       // ok

В частности, обратите внимание, что следующий код совершенно корректен:

Y y;
tuple<char(&)[10], Y&>(a, y); 

Возможно объявить тип кортежа, для которого нельзя построить объект. Это случается, если элемент, который не может быть инициализирован, Имеет меньший индекс, чем элемент, который требует инициализацию. К примеру, tuple<char[10], int&>.

В итоге, кортеж - это по сути просто группа индивидуальных элементарных конструкций.

Функция make_tuple

Кортежи могут быть созданы с помощью вспомогательной функции make_tuple (сравни 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>.

Иногда применение простых не-ссылочных типов нежелательно, к примеру если тип элемента не допускает копирование. Следовательно, должны быть возможности для программиста контролировать приведение типа и требовать, чтобы коллекция tuple использовала непосредственно ссылку на константный или не-константный тип элемента. Это выполняется с помощью двух вспомогательных шаблонных функции: ref и cref. Любой аргумент может быть передан с этими функциями для получения необходимого ссылочного типа. Данный механизм не противоречит сохранности константной квалификации, так как константный объект, переданный через ref, дает в результаты элемент коллекции tuple в виде константной ссылки (см. далее 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 по умолчанию приводится к ссылке на константный тип, поэтому массивы не нужно обертывать с помощью функции cref. К примеру:

make_tuple("Donald", "Daisy");

Данный код создает объект типа tuple<const char (&)[7], const char (&)[6]> (обратите внимание, что тип строкового литерала это массив константных символов, а не const char*). Однако, чтобы заставить функцию make_tuple создать кортеж с элементом типа не-константный массив, можно использовать обертку ref.

Указатели на функцию приводятся к простому не-ссылочному типу, то есть к простому указателю на функцию. Кортеж может также хранить ссылку на функцию, но такой кортеж не может быть сконструирован с помощью функции make_tuple (потому что в результате получится тип константной функции, что недопустимо):

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 это объект типа tuple и N это целочисленное константное выражение, задающее индекс элемента в кортеже. В зависимости от того, квалифицирован ли объект t как константный или нет, get возвращает N-й элемент как ссылку на константный или не-константный тип. Индекс первого элемента 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;                           // ошибка, нельзя присваивать константе 
  ...
double e = get<1>(t); // ok   
get<1>(t) = 3.14;     // ok 
get<2>(t) = A();      // ошибка, нельзя присваивать константе 
A aa = get<3>(t);     // ошибка: индекс вне диапазона 
  ...
++get<0>(t);  // ok, можно использовать как любую переменную

Обратите внимание, что функция get член класса не поддерживается с компилятором MS Visual C++. Более того, этот компилятор имеет проблемы с поиском функции get не члена класса без явной квалификацией пространства имен. Следовательно, при использовании компилятора MS VisualStudio C++ все вызовы функцииl get должны быть квалифицированы как tuples::get<N>(a_tuple).

Создание копированием и присваивание 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');

Операторы сравнения

Кортежи определяют операторы ==, !=, <, >, <= и >= через соответствующие операторы своих элементов. Это означает, что если любой из этих операторов определен для всех элементов двух кортежей, тогда этот же оператор определен и для самих кортежей. Операторы равенства для двух кортежей a и b определены так:

1. a == b если для каждого i справедливо ai == bi

2. a != b если существует такой индекс i, что ai != bi

Операторы <, >, <= и >= реализуют лексикографическое упорядочивание.

Обратите внимание, что попытка сравнить два кортежа различной длины приведет к ошибке времени компиляции.

Кроме этого, операторы сравнения следуют обычному для C/C++ принципу: сравнения элементов начинается с первого и продолжается только до первого ложного результата.

Примеры:

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..."

Связки (tiers)

Связки это кортежи, где все элементы являются не константными ссылками. Они создаются с помощью шаблонной функции tie (сравни с 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)).

Кортеж, который содержит не-константные ссылки как элементы, может быть использован для 'распаковки'  другого кортежа. К примеру:

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. Это очень удобно при вызове функций, которые возвращают объекты типа tuple.

Механизм связывания также работает с шаблонами типа  std::pair:

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

Объект ignore

В библиотеке Boost.Tuple есть также объект 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!)

Библиотека Boost.Tuples определяет три манипулятора для изменения поведения по умолчанию:

 

Обратите внимание, что эти манипуляторы определены в подпространстве имен tuples. К примеру:

cout << tuples::set_open('[') << tuples::set_close(']') << tuples::set_delimiter(',') << a; 

на выходе печатает тот же кортеж 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;

прочитает эти данные в кортежи i  j.

Обратите внимание, что извлечение кортежей с элементами типа std::string или C-строками в общем случае не работает, так как записанный в поток кортеж не может быть затем однозначно разобран обратно на элементы.

Эффективность

Все функции для доступа к элементами кортежей tuple и функции конструирования являются небольшими однострочными inline функциями. Поэтому современные компиляторы могут оптимизировать накладные расходы на операции с кортежами в сравнении с написанным вручную кодом с кортежами в виде классов. В частности, для современных компиляторов нет разницы в эффективности исполнения для кода:

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), которые не могут выполнить оптимизацию для такого использования кортежей.

В зависимости от оптимизирующих способностей компилятора, механизм связок может иметь небольшую потерю эффективности в сравнении с использованием не-константных ссылок как аргументов для передачи множества значений из функции. К примеру, предположим что следующие функции f1 и f2 имеют одинаковую функциональность:

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] для более детального обсуждения эффективности.

Эффекты времени компиляции

Компилирование кортежей может быть очень медленным из-за большого количества конкретизаций шаблонов. В зависимости от типа компилятора и длины кортежа, время компиляции может быть до 10 раз больше, чем при использовании написанных вручную классов, таких как приведенный ранее класс hand_made_tuple. Однако реальные программы обычно содержат много кода в дополнение к кортежам tuple, поэтому разница во времени компиляции оказывается несущественной. Для программ, которые используют кортежи очень часто, было зафиксировано увеличение времени компиляции от 5 до 10 процентов. Для этих же тестовых программ потребности в памяти для компиляции возрастали от 22% до 27%. См. также [1, 2].

Портабельность

The library code is(?) standard C++ and thus the library works with a standard conforming compiler. Below is a list of compilers and known problems with each compiler:

КомпиляторПроблемы
gcc 2.95-
edg 2.44-
Borland 5.5Нельзя использовать указатель на функцию или на член класса в качестве типа элемента кортежа
Metrowerks 6.2Нельзя использовать обертки ref и cref
MS Visual C++Ссылочные элементы не допустимы  (хотя tie работает). Нельзя использовать обертки ref и cref

Благодарности

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, David Abrahams found a way to get rid of most of the restrictions for compilers not supporting partial specialization. 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 2003-09-07

© 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.

последняя правка: 17.09.2005

библиотека BOOST C++ http://www.boost.org
перевод Elijah Koziev www.solarix.ru

  © Mental Computing 2010