Привет, Habr! Меня зовут Антон Митрохин, я позиционирую себя как очень молодой c++ разработчик. На момент написания статьи мне был 21 год, к тому времени я работал разработчиком CAE системы уже второй год. А еще я люблю с++ за все его минусы и плюсы ха-ха, люблю плюсы́ за плю́сы и хочу, чтобы вы тоже полюбили его.

Полтора года назад я заметил серьезное ухудшение своих когнитивных способностей. В один момент я просто не смог посчитать уравнение на подобии такого x = 13 + 6 * 8 + 2. Я мог вычислить части этого уравнения, например умножить 6 на 8, но когда моя мысль переключалась на следующее действие, я сразу забывал, что получилось на предыдущем шаге. Вычисление такого простого уравнения у меня заняло примерно 5 минут. Я уверен, что вы смогли бы посчитать, чему равен x, секунд за 10, а то и меньше. Я тоже ожидал от себя такой скорости, но получил неутешительный результат. Кстати, посчитать устно у меня так и не получилось, пришлось выписать на бумажку и записывать результат каждого действия.

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

Недавно у меня было очередное задание от психолога, нужно было перечислить вещи, которые мне нравятся. Я не хотел выполнять это задание, как, впрочем, и любое другое, что мне давали. Но начал. Начал по "стандартному списку". Гулять? - Не люблю. Смотреть фильмы? - Тоже нет. Компьютерные игрушки? - Нет, спасибо. Жить? - Пфф, точно не люблю. Когда пункты начали кончаться, я решил подойти к задаче с другой стороны и представить, как должна выглядеть моя жизнь, чтобы я ее полюбил. Озарение пришло быстро. Дом. Лес вокруг. В радиусе 5 километров ни души. Дровяная печь и компьютер. Зачем компьютер, тебе на работе не хватает? - спросил я себя. И тут меня озарило. Я ведь люблю программировать. Я обожаю свою работу, и готов заниматься программированием по 16 часов в сутки.

Раз я нашел, что мне в этой жизни приносит хоть какое-то удовольствие, то почему бы не начать творить? Так я решил написать цикл статей, в которых буду делиться своим опытом с Вами, Дорогими Читателями.

А кто такие эти ваши дескрипторы?

Вот так, внезапно, я перехожу к сегодняшней теме. Говорить мы будем о дескрипторах. А чтобы лучше понять, что это такое, да зачем оно надо, представим это на какой-то задачке. Для меня это более предпочтительный подход, чем сухие определения.

Теперь можно начать воображать. Значит так. Мы разрабатываем картографическое приложение 3gis. Поступает новое требование от заказчика: он хочет, чтобы можно было на карте города выбрать все дома определенного цвета, например желтые.

Да легко! - Говорим мы и сразу же беремся за работу.

Сначала нам нужно определить цвета. Так как мне лень определять большую палитру, на первое время определим только два цвета: желтый и любой не-желтый, я решил выбрать белый.

enum class color {yellow, white};

Потом нужно записать цвета домов на каждой улице. Пусть улица у нас будет массивом цветов. После массива цветов определим пару улиц, в которых будет всего по 4 дома, нам пока хватит.

using street = vector<color>;
street m_high_street = {white, white, yellow, white};
street m_low_street = {yellow, yellow, white, white};

Так, с улицами разобрались, теперь быстренько создадим город. Городом у нас будет массив улиц. Конечно, такая реализация городов и улиц в реальном приложении это большая ошибка проектирования, но, так как наше приложение 3gis никому не нужно только начинает набирать обороты, нам пока хватит и этого.

using city = vector<street>;
city m_city = {m_high_streen, m_low_streen};

Готово, теперь пишем функцию, которая будет проверять, желтый ли дом. Она работает очень просто: берем город, у города берем улицу номер street_number, а у этой улицы берем дом номер home_number. И проверяем, желтый ли цвет у этого дома.

bool is_yellow_home(size_t home_number, size_t street_number) {  
  return m_city[street_number][home_number] == color::yelow;
}

Осталось только вызвать функцию is_yellow_home из главной функции и проверить, компилируется ли.

void print_yellow_homes() {  
  for(size_t n_street = 0; n_street< m_city.size(); ++n_street) {    
    for(size_t n_home = 0; n_home < m_city[n_street].size(); ++n_home ) {      
      if(is_yellow_home(n_street, n_home)) {        
        cout << street_number << " - " << home_number << endl;      
      }    
    }  
  }
}

Вот теперь точно все. Даже компилируется успешно. Но есть одно но. В вызове функции is_yellow_home я перепутал местами аргументы. Если Вы внимательный читатель, то сразу заметили это, мое почтение! И вполне возможно, что ошибусь здесь не только я, но и другие разработчики. Тогда в голову приходит идея написать что-то такое:

using home_number_t = size_t;
using street_number_t = size_t;

bool is_yellow_home(home_number_t n_home, street_number_t n_street) {    
  return m_city[n_street][n_home] == color::yelow;
}

Но это нас никак не спасает, я все также, с успехом, могу перепутать аргументы и ничего не изменится. Нам нужна ошибка, если мы напутали с типами.

Сразу приходит на ум: нужно написать класс, который будет содержать size_t внутри себя. Точнее два класса. Один будет заменять тип home_number_t, а другой будет заменять street_number_t. Ну что, глаза боятся - руки делают.

Но сначала давайте определимся, что будет хранить базовый класс? Если конкретнее, только ли size_t он будет хранить? Я думаю, что вам не составит труда придумать случай, когда нужно будет хранить пару значений, а может и еще больше. А может вы вообще строку захотите хранить. Для того, чтобы иметь возможность хранить любой из придуманных типов, нужно найти у них что-то общее. Что же у них общего? Правильно - ничего =). Поэтому давайте напишем обертку над хранимым(и) типом(ами). Она, в свою очередь, будет классом, а хранить класс внутри базового класса дескриптора уже намного легче. Дефолтная обертка будет хранить тип size_t, а если кому-то понадобится хранить что-то другое, то он сам может написать обертку и передать ее в базовый класс.

struct default_descriptor_property {
  public:
    using value_type      = size_t;
    using difference_type = size_t;

  public:
    ~default_descriptor_property() = default;

    constexpr default_descriptor_property()                                              = default;
    constexpr default_descriptor_property(const default_descriptor_property&)            = default;
    constexpr default_descriptor_property(default_descriptor_property&&)                 = default;
    constexpr default_descriptor_property& operator=(const default_descriptor_property&) = default;
    constexpr default_descriptor_property& operator=(default_descriptor_property&&)      = default;

  public:
    // КОНСТРУКТОР от числа
    explicit constexpr default_descriptor_property(const size_t index) : _index(index) {
    }
    
  public:
    // ОПЕРАТОР ПРИВЕДЕНИЯ
    constexpr operator size_t() const {
      return _index;
    }

  private:
    value_type _index;
};

И, собственно, базовый класс

template<class SelectorType, class PropertyType = default_descriptor_property>
class descriptor_base {
  public:
    using property_type   = PropertyType;
    using difference_type = typename property_type::difference_type;

  public:
    ~descriptor_base() = default;

  public:
    constexpr descriptor_base() : _property(property_type{}) {
    }

  public:
    constexpr descriptor_base(const descriptor_base& other)            = default;
    constexpr descriptor_base(descriptor_base&& other)                 = default;
    constexpr descriptor_base& operator=(const descriptor_base& other) = default;
    constexpr descriptor_base& operator=(descriptor_base&& other)      = default;

  public:
    explicit constexpr descriptor_base(const property_type& prop) noexcept : _property(prop) {
    }

    template<class... Tys, std::enable_if_t<std::is_constructible_v<property_type, Tys...>, int> = 0>
    explicit constexpr descriptor_base(Tys&&... values) noexcept : _property(std::forward<Tys>(values)...) {
    }

  public:
    constexpr operator size_t() const {
      return static_cast<size_t>(_property);
    }

  private:
    property_type _property;
};

Тут хотелось бы сделать важное замечание.

При объявлении класса, я использовал два параметра шаблона SelectorType и PropertyType.

template<class SelectorType, class PropertyType = default_descriptor_property> 
class descriptor_base;

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

class home_selector {};
class street_selector {};

using home_descriptor    = descriptor_base<home_selector>;
using street_descriptor  = descriptor_base<street_selector>;

И в итоге мы получаем одни плюсы: реализацию нам нужно написать только один раз, а компилятор типы home_descriptor и street_descriptor считает абсолютно разными типами.

Перепишем нашу функцию на реализацию с работой с дескрипторами, получается

bool is_yellow_home(home_descriptor home_number, street_descriptor street_number) {  
  return m_city[street_number][home_number] == color::yelow;
}

А шо по скорости?

Настала, наконец, моя любимая часть работы. Бенчмаркать!

Давайте сравним, насколько медленнее стал наш код. Для этого достаточно (?) написать бенчмарк, который в одной реализации создает число типа size_t а в другой - home_descriptor.

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

Хочется рассказать, как я вообще решил провернуть такую вещь и откуда у меня была уверенность, что при оптимизации дескриптор превратится в простое число.

Я всегда любил смотреть, как реализована стандартная библиотека в VisualStudio, можно сказать, что я на этом и научился программировать. И вот, однажды, я заметил, что все конструкторы, которые принимают в качестве аргумента std::initializer_list, принимают его по значению, а не по ссылке. Вот пример реализации конструктора vector.

vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
        : _Mybase(_Al)
        {   // construct from initializer_list, optional allocator
        _Range_construct_or_tidy(_Ilist.begin(), _Ilist.end(), random_access_iterator_tag{});
        }

Тогда я, естественно, задал вопрос на всем известном сайте, почему std::initializer_list передается по значению, ведь передавать класс по ссылке быстрее. Оказалось, что есть магические типы, которые с помощью оптимизаций компилятора из класса превращаются в более простые типы. Опытным путем мне удалось выяснить, что такая оптимизация работает со всеми типами, которые относятся к std::is_fundamental. Вот в такой ассемблерный код превращается создание нашего дескриптора, что абсолютно эквивалентно созданию числа.

       push   %rbp
       push   %r14
       push   %rbx
       mov    %rdi,%r14
       mov    0x1a(%rdi),%bpl
       mov    0x10(%rdi),%rbx
       callq  2112d0 <benchmark::State::StartKeepRunning()>
       test   %bpl,%bpl
       jne    210b7c <descriptor(benchmark::State&)+0x3c>
       test   %rbx,%rbx
       je     210b7c <descriptor(benchmark::State&)+0x3c>
       xor    %eax,%eax
0.56%  mov    $0x400,%ecx
0.17%  nopw   %cs:0x0(%rax,%rax,1)
       nop
98.20% add    $0xffffffffffffffff,%rcx
0.04%  jne    210b70 <descriptor(benchmark::State&)+0x30>
1.02%  add    $0xffffffffffffffff,%rbx
       jne    210b60 <descriptor(benchmark::State&)+0x20>
       mov    %r14,%rdi
       pop    %rbx
       pop    %r14
       pop    %rbp
       jmpq   2113b0 <benchmark::State::FinishKeepRunning()>

Надеюсь, что после прочтения статьи вам стало понятно, что такое дескриптор и когда он нужен. Конечно, это очень простое приминение и простая реализация, какой-нибудь файловый дескриптор может быть написан на пару тысяч строк. Я ни в коем случае не претендую на самую лучшую реализацию. Единственное, на что я претендую, так это на Ваши читательские симпатии :3. Жду конструктивной критики!

Если Вам интересно посмотреть более обширную реализацию этих классов, то я выложил код на GitHub.

Комментарии (21)


  1. rsashka
    21.02.2022 10:39
    +5

    Может лучше спорт?


    1. Vladimir_Putin Автор
      21.02.2022 15:55

      Да, спорт тоже не помешает, решил с этого дня по вечерам гулять с женой. Благодарю за дельный совет!


  1. ionicman
    21.02.2022 11:34

    Я, конечно, не всамделишный Си-шник, а typedef-ом нельзя было проблему решить?
    Задать отдельный тип для номера дома и отдельный для номера улицы?


    1. farafonoff
      21.02.2022 12:51
      +3

      typedef создает alias на тип, а не новый тип. Т.е. компилятор не различит два тайпдефа на size_t.


      1. lamerok
        21.02.2022 14:31
        -2

        А можно было бы просто унаследоваться?


      1. ionicman
        21.02.2022 14:38

        А так?

        typedef struct {
        	int value;
        } A;
        
        typedef struct {
        	int value;
        } B;
        
        void f (A a, B b) {
        	cout << a.value << " " << b.value;
        }
        
        int main () {
        	A a = { value:5 };
        	B b = { value:7 }; 
        
        	f (b, a);
        	
        	return 0;
        }
        


        У меня ругается на аргументы.


        1. Vladimir_Putin Автор
          21.02.2022 15:54
          +1

          Я не совсем понял, что вы хотели спросить, но постараюсь ответить. При таком способе есть минус - нужно реализовывать несколько структур, а это громоздко и слишком уж лень это писать и поддерживать. И typedef-ом это нельзя решить, я же в статье написал, что typedef-ы компилятор считает за один тип, поэтому, собственно и пришлось придумывать, как можно выкрутиться.


  1. olsowolso
    21.02.2022 12:22

    Спасибо за статью.

    У меня возник вопрос вот по этой конструкции:

    template<class... Tys,

    а именно "class..." - что в этом месте (и последующих) означает троеточие? Это часть синтаксиса языка? Поиск в гугле не дал внятного разъяснения.


    1. Biga
      21.02.2022 12:26
      +6

      Это variadic template - шаблон функции с переменным числом аргументов.


  1. otto_haendler
    21.02.2022 12:43

    Спорт убивает любую депрессию, абсолютно любую. Очень рекомендую поменять расходы на психолога на расходы на фитнес зал или беговую дорожку или просто спорт-инвентарь. Автору респект за статью и удачи в борьбе с депрессией.


    1. MaYeDum
      21.02.2022 13:30
      +3

      Не совсем, спорт может убить "депрессию", которая на самом деле грусть, плохое настроение, выгорание, усталость. И то, если для человека спорт - это не дикий труд, который может заставить эмоционально задолбаться ещё больше.
      Спорт (смотря какой) - это один из инструментов либо для профилактики, либо для облегчения симптомов, но вместе с психотерапией,медикаментами или чем-то другим.

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


      1. otto_haendler
        21.02.2022 16:13

        Я все же голосую за спорт.


    1. Vladimir_Putin Автор
      21.02.2022 15:50

      Большое спасибо за совет. С этого дня каждый вечер гуляем с женой после работы!


  1. kompilainenn2
    21.02.2022 16:08
    +1

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


    1. farafonoff
      21.02.2022 16:23
      +3

      Весна


    1. Vladimir_Putin Автор
      21.02.2022 16:29
      +2

      Депрессия — заболевание, распространенное во всем мире. Согласно отчетам ВОЗ, им страдают порядка 280 млн человек, или около 4% жителей планеты. В России, по данным Министерства здравоохранения, депрессией страдают около 15 млн жителей — фактически каждый десятый.

      Вероятность того, что статью напишет человек с депрессией ~1:10, отсюда и тенденция, наверное. Не знаю, что вас удивило.


      1. Nagdiel
        21.02.2022 21:15

        Это всего лишь вероятность того, что выбранный наугад житель РФ страдает депрессией… если считать приведенные Вами цифры достоверными. Вероятность наличия депрессии в отдельно взятой категории жителей РФ — «авторов статей на Хабре» — может оказаться в корне отличной.


    1. UberSchlag
      21.02.2022 20:09
      +2

      Ну, имхо, тут простая система. Если коротко: один решился высказаться - помог другим решиться.

      Большая психиатрия (да и большая часть малой) - до сих стигматизирована. Человек, психика которого начинает выходить из баланса, с определённой степени серьёзности состояния начинает ощущать свою "нетаковость/неправильность". Непрошаренные в психпросвете близкие и друзья от попыток поделиться ношей могут дать эмоциональный отлуп в виде "да ты не парься, нужно ПРОСТО %простометод_решения_проблем%, из лучших побуждений конечно же. В итоге человек закрывается на эту тему и варится со своими демонами в голове один. До решения идти к специалисту ещё нужно дорасти. Высказаться, хотя бы в виде статьи - эдакий шажок к людям и может заметно помочь спустить пар, услышать какую-то вменяемую поддержку, почувствовать толику общности.

      Ну и в сабжевом посте вполне себе технический материал, так что всё ок %)


  1. Ritan
    21.02.2022 17:26
    +5

    По сути это довольно известный(в узких кругах) паттерн strong type(или strong typedef)


  1. orcy
    23.02.2022 09:28

    63


  1. Antervis
    23.02.2022 17:58

    enum class descriptor : size_t {};