Что такое traits?

ОГЛАВЛЕНИЕ

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

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

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

Теперь следует дать определение свойств. Натан Майерс, разработавший метод использования свойств, предложил такое определение:

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

Определение не настолько понятное, так что давайте попробуем разобраться, что же имеется в виду. Для этого предлагается рассмотреть небольшой пример. В качестве примера мы рассмотрим шаблонный класс динамического массива. Конечно, реализовывать полностью этот класс мы не будем(у нас уже есть vector), но общие концепции мы рассмотрим.

Итак, наш шаблонный класс динамического массива должен иметь в качестве аргументов шаблона:

  1. тип элемента шаблона
  2. тип ссылки на элемент
  3. тип аргумента функций
  4. тип константной ссылки

Думаю, прокомментировать стоит только тип аргумента функций. Этот тип используется для вставки элементов в массив. Например, эффективней передать int или char по значению, чем по константной ссылке.

Тогда набросок класса будет выглядеть так:

template <typename T,
          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<int> vec1; // эквивалентно: vector<int, const int&, int&, const int&>

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

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

// используем параметры по умолчанию
vector<char> vec4; // эквивалентно: vector<char, const char&, char&, const char&>
Все хорошо, все отлично работает. Но если мы захотим реализовать, например, связанный список, то нам придется для него задавать аналогичные параметры шаблона. Это довольно муторно, так как придется каждый раз писать одно и то же. Тогда на помощь приходят классы свойств(traits). Создадим шаблон класса, который будет содержать все те дополнительные аргументы шаблона:
// первичный шаблон
// подходит в общем случае - аналог аргументов шаблона по умолчанию
template <typename T>
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 T,
          typename traits = elem_traits<T> > // свойство по умолчанию
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<int> vec1; // эквивалентно: vector<int, elem_traits<int> >
// тогда:
// arg_type  = const int&
// reference = int&
// const_reference = const int&

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

Тогда объекты нашего динамического массива будем создавать так:

vector<std::string> vec1; // эквивалентно: vector<std::string, elem_traits<std::string> >
// для всех типов, для которых не сделана специализация,
// все остается как прежде:
// тогда:
// arg_type  = const std::string& (по ссылке)
// reference = std::string&
// const_reference = const std::string&


// а для типа char мы сделали специализацию
vector<char> vec2; // эквивалентно: vector<char, elem_traits<char> >
// тогда:
// 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<char, char_elem_traits> vec;
// тогда:
// arg_type  = char& (по ссылке, но не по константной)
// reference = char&
// const_reference = const char&
Но заметим, что класс(структура) char_elem_traits переопределяет только один тип - arg_type, а остальные остаются неимзенными по отношению к elem_traits<char>. То есть мы произвели лишнюю работу, определив самостоятельно типы reference и const_reference. Хорошо еще, что тут немного типов, а представьте, что их было бы около 20? 50? Чтобы каждый раз не переписывать общие свойства, можно воспользоваться открытым наследованием и переопределить нужные нам типы:
class char_elem_traits : public elem_traits<char> {
 public:
  typedef char& arg_type; // определили тип, который передает по ссылке типы char

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

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

До этого мы рассматривали только свойства, определяющие необходимые типы. Еще могут быть свойства-значения: они предоставляют нужные константы для данного типа.

Рассмотрим пример: нам надо написать шаблон класса, которому для каждого параметра шаблона(типа) требуются связанные с ним константы. Первая мысль будет такой(пример немного перефразирован из вопроса с форума):

template <typename T,
          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 <typename T>
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 <typename T, typename traits = x_traits<T> >
class X {
  // используем нужные константы через traits:
  // traits::T2 == sizeof(T) * 4
  // traits::T4 == sizeof(T)
};
Но теперь в нашем коде возникает проблема: если вдруг получится так, что пользователь нашего класса захочет взять адрес нашей константы, компилятор должен будет создать реальную константу в памяти, адрес которой можно взять. Для этого был предуман трюк с enum'ом:
template <typename T>
struct x_traits {
 enum { T2 = sizeof(T) * 4 };
 enum { T3 = sizeof(T) * 2 };
 enum { T4 = sizeof(T)      };
};

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

Дело в том, что компилятор не позволяет взять адрес enum-констант. Так что мы теперь точно знаем, что наша константа будет вставлена в код числом.

На данном этапе все хорошо. Но вдруг нам надо доработать класс так, что нужна константа вещественного типа. Но возникает такая проблема: в классах можно инициализировать только статические интегральные константы. В enum'ах тоже можно использовать только целые типы. Что же тогда делать? Остается только определить inline-фунцию, которая бы возвращала нужное значение:

template <typename T>
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 <typename T, typename traits = x_traits<T> >
class X {
  // используем нужные константы через traits:
  // T2: traits::T2() - возвращает тип std::size_t, можно и через enum
  // T5: traits::T5() - возвращает тип double, по-другому не сделать, только функция
};
Надо заметить, что наличие функции на производительность не влияет, так как современные компиляторы способны подставить нужное значение прямо в код вместо вызова функции. Также можно сочетать наличие функций, возвращающих нужные значения вещественного типа, и простые интегральные константы, полученные с помощью enum.