Что такое 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

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

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