Что такое traits?

автор , Дек.14, 2010, рубрики C/C++/C#

Что такое traits?

Вступление
В данной статье я попытаюсь рассказать, что такое traits. Будут рассмотрены некоторые примеры применения traits, которые будут заключаться как в использовании traits в на�?ем коде, так и в возможных способах рас�?ирения стандартной библиотеки C++, которая тоже использует traits. Также будут рассмотрены возможные проблемы, которые могут возникнуть при рас�?ирении стандартной библиотеки C++.

Для кого написана данная статья?
Эта статья написана для программистов на C++, которые уже неплохо владеют самим языком, его основными конструкциями. В частности, необходимо знание, что такое �?аблоны(templates) и желателен опыт их использования. Также очень желательно знание стандартной библиотеки C++, так как многие примеры будут посвящены именно ей.

Ну, поехали...
�?так, приступим. Думаю, начать стоит с перевода термина traits. Обычно его переводят как "свойства". Но traits реализуются классом, поэтому обычно употребляется термин "класс свойств". Следует заметить, что свойства также можно реализовать с помощью структуры, так как в C++ это практически аналоги. Далее я буду использовать термин класс, хотя все сказанное будет в той же мере относиться к структурам.

Теперь следует дать определение свойств. Натан Майерс, разработав�?ий метод использования свойств, предложил такое определение:
Класс свойств - это класс, используемый вместо параметров �?аблона. В качестве класса он объединяет полезные типы и константы; как �?аблон, он является средством для обеспечения того "дополнительного уровня косвенности", который ре�?ает все проблемы программного обеспечения.

Определение не настолько понятное, так что давайте попробуем разобраться, что же имеется в виду. Для этого предлагается рассмотреть неболь�?ой пример. В качестве примера мы рассмотрим �?аблонный класс динамического массива. Конечно, реализовывать полностью этот класс мы не будем(у нас уже есть vector), но общие концепции мы рассмотрим.
�?так, на�? �?аблонный класс динамического массива должен иметь в качестве аргументов �?аблона:
1) тип элемента �?аблона
2) тип ссылки на элемент
3) тип аргумента функций
4) тип константной ссылки
Думаю, прокомментировать стоит только тип аргумента функций. Этот тип используется для вставки элементов в массив. Например, эффективней передать int или char по значению, чем по константной ссылке.
Тогда набросок класса будет выглядеть так:
template
typename ArgT = const T&,
typename RefT = T&,
typename ConstRefT = const T&>
class vector {
// ...
public:
typedef T value_type;
typedef ArgT arg_type;
typedef RefT reference;
typedef ConstRefT const_reference;

void push_back(arg_type);

// ...
};

Для удобства были добавлены соответствующие значения параметров �?аблона по умолчанию.
Тогда каждый пользователь на�?его класса должен будет создавать объекты класса как-то так:
// используем параметры по умолчанию
vector vec1; // эквивалентно: vector

// переопределяем один из параметров по умолчанию
// обратите на второй аргумент �?аблона(не ссылка, а передача по значению)
vector vec2; // эквивалентно: vector

// переопределяем один из параметров по умолчанию
vector vec3; // // эквивалентно: vector

// используем параметры по умолчанию
vector vec4; // эквивалентно: vector

Все хоро�?о, все отлично работает. Но если мы захотим реализовать, например, связанный список, то нам придется для него задавать аналогичные параметры �?аблона. Это довольно муторно, так как придется каждый раз писать одно и то же. Тогда на помощь приходят классы свойств(traits). Создадим �?аблон класса, который будет содержать все те дополнительные аргументы �?аблона:
// первичный �?аблон
// подходит в общем случае - аналог аргументов �?аблона по умолчанию
template
class elem_traits {
public:
typedef const T& arg_type;
typedef T& reference;
typedef const T& const_reference;
};

Это и будет на�? класс свойств. То есть он описывает те типы(arg_type, reference, const_reference), которые представляют на�? тип T. Таким образом, нам надо вместо несколько аргументов �?аблона писать только один дополнительный аргумент - класс свойств, который содержит в себе все нужные типы.
Тогда класс на�?его динамического массива можно переписать так:
template
typename traits = elem_traits > // свойство по умолчанию
class vector {
// ...
public:
typedef T value_type;
typedef typename traits::arg_type arg_type;
typedef typename traits::reference reference;
typedef typename traits::const_reference const_reference;

void push_back(arg_type);

// ...
};

Тогда давайте посмотрим, как пользователи на�?его динамического массива будут создавать объекты:
// используется аргумент-свойство по умолчанию
vector vec1; // эквивалентно: vector >
// тогда:
// arg_type = const int&
// reference = int&
// const_reference = const int&

// используется аргумент-свойство по умолчанию
vector vec1; // эквивалентно: vector >
// тогда:
// arg_type = const char&
// reference = char&
// const_reference = const char&

В этом примере для всех типов используются аргументы по умолчанию. Но мы выяснили, что аргументы типа char луч�?е передавать не по константной ссылке, а по значению, то можно сделать специализацию на�?его класса свойств:
// специализация для типа char
template <>
class elem_traits {
public:
typedef const char arg_type; // определили тип, который передает по значению типы char
typedef char& reference;
typedef const char& const_reference;
};

Тогда объекты на�?его динамического массива будем создавать так:
vector vec1; // эквивалентно: vector >
// для всех типов, для которых не сделана специализация,
// все остается как прежде:
// тогда:
// arg_type = const std::string& (по ссылке)
// reference = std::string&
// const_reference = const std::string&

// а для типа char мы сделали специализацию
vector vec2; // эквивалентно: vector >
// тогда:
// arg_type = const char (по значению, а не по ссылке)
// reference = char&
// const_reference = const char&

Давайте теперь рассмотрим случай, когда мы хотим создать динамический массив с элементами типа char, но чтобы arg_type был эквивалентен char&. Специализация на�?его класса elem_traits для char уже существует, то есть ее сделать мы уже не можем. В таком случае остается создать новый класс свойств:
// обратите внимание: структура, а не класс
// (разницы никакой, это ли�?ний раз подчеркивается данным примером)
struct char_elem_traits {
// здесь переопределеяем тип аргумента функций.
// Мы договорились, что это будет char&
typedef char& arg_type; // определили тип, который передает по ссылке типы char

// остальное оставляем так же.
// Хотя ничто не ме�?ает нам переопределеить тип ссылки или константной ссылки
typedef char& reference;
typedef const char& const_reference;
};

Тогда осталось только создать нужный динамический массив:
vector vec;
// тогда:
// arg_type = char& (по ссылке, но не по константной)
// reference = char&
// const_reference = const char&

Но заметим, что класс(структура) char_elem_traits переопределяет только один тип - arg_type, а остальные остаются неимзенными по отно�?ению к elem_traits. То есть мы произвели ли�?нюю работу, определив самостоятельно типы reference и const_reference. Хоро�?о еще, что тут немного типов, а представьте, что их было бы около 20? 50? Чтобы каждый раз не переписывать общие свойства, можно воспользоваться открытым наследованием и переопределить нужные нам типы:
class char_elem_traits : public elem_traits {
public:
typedef char& arg_type; // определили тип, который передает по ссылке типы char

// типы reference и const_reference наследуются
};

Теперь можно использовать на�? класс свойств char_elem_traits точно так же, как мы делали это рань�?е.

До этого мы рассматривали только свойства, определяющие необходимые типы. Еще могут быть свойства-значения: они предоставляют нужные константы для данного типа.
Рассмотрим пример: нам надо написать �?аблон класса, которому для каждого параметра �?аблона(типа) требуются связанные с ним константы. Первая мысль будет такой(пример немного перефразирован из вопроса с форума):
template
std::size_t T2 = sizeof(T) * 4,
std::size_t T3 = sizeof(T) * 2,
std::size_t T4 = sizeof(T)
// ...
>
class X {
// используем нужные константы
// T2 == sizeof(T) * 4
// T4 == sizeof(T)
};

Но мы теперь люди продвинутые и знаем, как избежать такого боль�?ого количества аргументов �?аблона - обернуть все в traits:
template
struct x_traits {
static std::size_t T2 = sizeof(T) * 4;
static std::size_t T3 = sizeof(T) * 2;
static std::size_t T4 = sizeof(T);
};

template >
class X {
// используем нужные константы через traits:
// traits::T2 == sizeof(T) * 4
// traits::T4 == sizeof(T)
};

Но теперь в на�?ем коде возникает проблема: если вдруг получится так, что пользователь на�?его класса захочет взять адрес на�?ей константы, компилятор должен будет создать реальную константу в памяти, адрес которой можно взять. Для этого был предуман трюк с enum'ом:
template
struct x_traits {
enum { T2 = sizeof(T) * 4 };
enum { T3 = sizeof(T) * 2 };
enum { T4 = sizeof(T) };
};

template >
class X {
// используем нужные константы через traits:
// traits::T2 == sizeof(T) * 4
// traits::T4 == sizeof(T)
};

Дело в том, что компилятор не позволяет взять адрес enum-констант. Так что мы теперь точно знаем, что на�?а константа будет вставлена в код числом.
На данном этапе все хоро�?о. Но вдруг нам надо доработать класс так, что нужна константа вещественного типа. Но возникает такая проблема: в классах можно инициализировать только статические интегральные константы. В enum'ах тоже можно использовать только целые типы. Что же тогда делать? Остается только определить inline-фунцию, которая бы возвращала нужное значение:
template
struct x_traits {
static std::size_t T2() {
return(sizeof(T) * 4);
}

static std::size_t T3() {
return(sizeof(T) * 2);
}

static std::size_t T4() {
return(sizeof(T));
}

// возвращаемое значение типа double
static double T5() {
return(sizeof(T) * 5 / 7);
}
};

template >
class X {
// используем нужные константы через traits:
// T2: traits::T2() - возвращает тип std::size_t, можно и через enum
// T5: traits::T5() - возвращает тип double, по-другому не сделать, только функция
};

Надо заметить, что наличие функции на производительность не влияет, так как современные компиляторы способны подставить нужное значение прямо в код вместо вызова функции. Также можно сочетать наличие функций, возвращающих нужные значения вещественного типа, и простые интегральные константы, полученные с помощью enum.

Теперь мы знаем основные принципы для работы с traits. Так что давайте рассмотрим пример, который помогает рас�?ирить стандартную библиотеку C++.
Давайте рассмотрим такую задачу(перефразировано из вопроса с форума):
Цитата
Я хочу определить размер файла с помощь класса ifstream. Сам файл весит боль�?е 5 Гб. Функция tellg() возвращает какое-то нереальное значение. Как можно правильно определить размер файла?

На данный момент все известные мне версии стандартной библиотеки C++ представляют позицию в файле 32-разрядным целым. Но дело в том, что обычные 32-разрядные целые числа не могут представлять размер файла, боль�?его 4 ГБ(происходит переполнение). То есть нам надо каким-либо образом заставить стандартную библиотеку использовать не 32-разрядные числа, а, например 64-разрядные(или вообще на�? собственный тип(класс), который мы опи�?ем). Как это сделать? Как вы уже догадались, помогут нам traits.
Как известно, ifstream - это только typedef от класса basic_ifstream. Сам же класс basic_ifstream принимает 2 параметра �?аблона: первый из них определяет тип символа, а второй определяет свойста(traits). Так вот эти свойста и должны определять, каким типом представлять позицию в файле, как сравнивать символы и тд. Второй параметр �?аблона класса basic_ifstream по умолчанию будет классом char_traits. Это стандартный класс, который описывает основные свойста: нужные типы, как сравнивать символы, присваивать и тд.. Так как мы не собираемся переопределять это все(нам надо заменить только 2 типа), тогда хоро�?ей идеей будет унаследоваться от класса char_traits.
У класса char_traits есть 2 интересующих нас типа(полный список типов можно найти в документации):
1) pos_type - тип, используемый для представления позиции в потоке
2) off_type - тип, используемый для представления смещений между позициями в потоке
Вот их-то как раз нам и надо переопределить. Давайте сделаем первую попытку:

template
struct long_pointer_traits : public std::char_traits {
typedef __int64 pos_type;
typedef __int64 off_type;
};

typedef std::basic_ifstream > long_ifstream;

// используем long_ifstream

Но вот незадача: этот код не компилируется. Дело в том, что pos_type должен уметь конструироваться из нескольких заранее определенных типов(как показало исследование, 2). Базовые типы этого делать не умеют, так что придется написать свой собственный класс. Я не буду заострять внимание на этом классе, так как статья немного не на эту тему. Я просто приведу реализацию этого класса, а если у вас будут какие-то вопросы, то писать либо здесь, либо в PM. �?так, вот код:
// пространство имен, в которое заносятся детали реализации
namespace detail {
template
class pos_type_t {
typedef pos_type_t my_type;

num_type m_pos;
state_type m_state;

static state_type initial_state;

public:
// конструкторы
pos_type_t(std::streampos off) : m_pos(off), m_state(initial_state) {}
pos_type_t(num_type off = 0) : m_pos(off), m_state(initial_state) {}
pos_type_t(state_type state, num_type pos) : m_pos(pos), m_state(state) {}

// получение состояния потока
state_type state() const {
return(m_state);
}

// установка состояния потока
void state(state_type st) {
m_state = st;
}

// получение позиции
num_type seekpos() const {
return(m_pos);
}

// оператор преобразования
operator num_type() const {
return(m_pos);
}

// далее идут операторы, которые осуществляют арифметические операции

num_type operator- (const my_type& rhs) const {
return(static_cast(*this) - static_cast(rhs));
}

my_type& operator+= (num_type pos) {
m_pos += pos;
return(*this);
}

my_type& operator-= (num_type pos) {
m_pos -= pos;
return(*this);
}

my_type operator+ (num_type pos) const {
my_type tmp(*this);
return(tmp += pos);
}

my_type operator- (num_type pos) const {
my_type tmp(*this);
return(tmp -= pos);
}

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

bool operator== (const my_type& rhs) const {
return(static_cast(*this) == static_cast(rhs));
}

bool operator!= (const my_type& rhs) const {
return(!(*this == rhs));
}
};
//---------------------------------------------------
// статическая константа, которая обозначает начальное состояние
template
state_type pos_type_t::initial_state;
}
//---------------------------------------------------
// наконец-то на�? класс свойств:
template
struct long_pointer_traits : public std::char_traits {
// определение pos_type через на�? только что написанный класс
typedef detail::pos_type_t pos_type;

// определение off_type через тип, переданный во 2 аргументе �?аблона
typedef long_pos_t off_type;
};
//---------------------------------------------------
// вводим тип "длинного" файла
typedef std::basic_ifstream > long_ifstream;

// используем long_ifstream для получения размера файла

ОК, теперь все компилируется и работает. Но кроме получения позиции в файле, нам обычно надо работать еще с этими файлами(читать, писать). �?, конечно, нам приходится работать со строками. Тогда если мы попытаемся считать строку из файла таким образом:
long_ifstream infile(strFileName, std::ios::binary);
std::string res;
std::getline(infile, res);

То мы получим о�?ибку компиляции. Проблема в том, что std::string - это "всего ли�?ь" typedef от std::basic_string. Этот класс принимает 2 параметра �?аблона: первый - тип для представления символа, а второй(как вы уже, наверное, догадались) - traits. Так вот, для корректной работы нам надо определить и свой тип строки:
// "длинные" типы:
typedef std::basic_ifstream > long_ifstream;
typedef std::basic_string > long_string;

long_ifstream infile(strFileName, std::ios::binary);
long_string res;
std::getline(infile, res);

Теперь все работает прекрасно. Таким образом, для правильного взаимодействия компонентов стандартной библиотеки нам придется определять нужные типы и работать с ними. К сожалению, на данный момент я не знаю способа, как можно было бы создать нужный тип для стандартных потоков ввода/вывода(cin, cout, cerr, clog). Так что чтобы вывести такую "длинную" строку на экран, надо будет написать свой оператор вывода такой строки. Другого ре�?ения мне неизвестно(если кто-то знает - поделитесь, буду признателен).
Также хочу сказать несколько слов о совместимости и переносимости: приведенный мной код по определению размера боль�?ого файла был проверен на компиляторах VC7.1 и Intel C++ 8.0. �?спользовалась стандартная библиотека, которая идет по умолчанию с VC. При работе с ней замечено никаких о�?ибок не было. Проверялся код и с использованием STLPort версий 4.6.2 и 5.0. Компилировался он без проблем, но работал неправильно. Надеюсь, в дальней�?их версиях STLPort'а это будет исправлено и работать будет корректно, так как данный код соответствует стандарту.

Ну вот, вроде бы и все, что я хотел сообщить по поводу свойств. Надеюсь, данная статья помогла вам разобраться, что это такое и зачем оно надо. Также жду ва�?и отзывы, комментарии и критику.



Что-то ищите?

Используйте форму для поиска по сайту:



Все еще не можете что-то найти? Оставьте комментарий или свяжитесь с нами, тогда мы позаботимся об этом!

Ключевые слова нашего блога

  • Ускорение windows xp
  • Активация windows xp
  • Виндовс XP
  • Оптимизация windows xp
  • Активировать windows xp
  • Активация виндовс xp
  • Активация windows xp sp3
  • Скачать windows xp sp3
  • Настройка windows xp
  • Тонкая настройка windows xp

Архив сообщений

Все вхождения, в хронологическом порядке...