суббота, 17 сентября 2022 г.

[prog] В склерозник: статья с поверхностным разбором принципов решения задач на coding interview

В кои-то веки в дайджесте от Medium проскочила интересная ссылка: Popular Problem-Solving Approaches in Data Structures and Algorithms.

В ней кратко описываются несколько категорий подходов к решению алгоритмических задачек, которые любят использовать на собеседованиях. Плюс ссылки на примеры этих самых задач с разборами возможных решений (я так понял, что все ссылки ведут на https://www.enjoyalgorithms.com/coding-interview/).

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

пятница, 16 сентября 2022 г.

[prog.c++] Хочется странного: структуры, которые можно инициализировать только посредством designated initializers

В последние несколько лет ощущаю за собой пунктик по поводу type-safety. Поэтому стараюсь писать код так, чтобы компилятор был в состоянии отследить и предотвратить некоторые ошибки. Не всегда это получается в принципе, а там, где получается, не всегда получается хорошо. Но попыток не оставляю.

Например, одна из вещей, которая вызывает серьезные подозрения и привлекает мое повышенное внимание -- это последовательность аргументов одного типа. Вроде чего-то такого:

frames_buffer_t make_buffer(size_t frame_size, size_t frames_count);

При вызове такой функции запросто можно перепутать аргументы местами и написать:

auto buf = make_buffer(32, 1024);

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

Это касается не только подряд идущих аргументов одного типа, тоже самое можно отнести и к структурам, в которых подряд идут однотипные поля. Вроде вот такого:

struct capture_params_t {
  size_t m_frame_size;
  size_t m_frames_count;
  ...
};

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

capturing_stream_t make_stream(const capture_params_t & params) {
  auto buf = make_buffer(params.m_frames_count, params.m_frame_size);
  ...
}

Или даже так:

capture_params_t tune_params(const capture_params_t & default_values) {
  capture_params_t result{ default_values };
  if(has_enough_memory()) {
    result.m_frame_size = default_values.m_frames_count * 4u;
    ...
  }
  ...
  return result;
}

Тогда как в случае использования даже примитивных strong typedef подобные ошибки самим компилятором отлавливаются на раз. Ну и при чтении кода как-то все более понятно. ИМХО, конечно же.

Самый простой вариант в C++ после C++11 -- это что-то вроде:

struct frame_size_t { size_t m_value; };
struct frames_count_t { size_t m_value; }

frames_buffer_t make_buffer(frame_size_t frame_size, frames_count_t frames_count);

Что приводит к тому, что в коде уже приходится писать так:

auto buf = make_buffer(frame_size_t{32}, frames_count_t{1024});

И тут уже глядя на код можно задуматься, а нормально ли, что у нас в буфере большое количество маленьких фреймов? Может быть должно быть наоборот?

В общем, в последнее время пытаюсь регулярно применять strong typedef в том или ином виде.

Но время от времени сталкиваюсь со случаями, когда введение strong typedef в виде отдельных типов выглядит как overkill.

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

class probe_result_t
{
    friend class data_collector_t;

    int m_read_pos;
    int m_write_pos;
    int m_size;

    probe_result_t(int read_pos, int write_pos, int size)
        : m_read_pos{read_pos}, m_write_pos{write_pos}, m_size{size}
    {}

public:
    [[nodiscard]] int size() const noexcept { return m_size; }
};

И вот у него в конструктор передаются параметры одного типа, что мне не нравится.

Но и не хочется превращать этот простой класс во что-то подобное:

class probe_result_t
{
    friend class data_collector_t;

    struct read_pos_t { int m_v; };
    struct write_pos_t { int m_v; };
    struct size_t { int m_v; };

    int m_read_pos;
    int m_write_pos;
    int m_size;

    probe_result_t(read_pos_t read_pos, write_pos_t write_pos, size_t size)
        : m_read_pos{read_pos.m_v}, m_write_pos{write_pos.m_v}, m_size{size.m_v}
    {}

public:
    [[nodiscard]] int size() const noexcept { return m_size; }
};

Как по мне, так отличным решением здесь могло бы стать использование вспомогательной структуры и добавленных в C++20 designated initializers:

class probe_result_t
{
    friend class data_collector_t;

    struct params_t
    {
        int m_read_pos;
        int m_write_pos;
        int m_size;
    };

    params_t m_v;

    probe_result_t(params_t params)
        : m_v{params}
    {}

public:
    [[nodiscard]] int size() const noexcept { return m_v.m_size; }
};

Создавать экземпляр probe_result_t можно было бы просто и наглядно:

probe_result_t data_collector_t::probe_collected_data()
{
    ...
    return { { .m_read_pos = rp, .m_write_pos = wp, .m_size = ds } };
}

Но вот беда... Нельзя в C++ указать, что экземпляр структуры можно проинициализировать только посредством designated initializers. Старый-добрый initializer list вполне себе будет работать:

probe_result_t data_collector_t::probe_collected_data()
{
    ...
    // Так можно написать, а хотелось бы, чтобы это было под запретом.
    return { { rp, wp, ds } };
}

Наверное, мне таки нужны именованные аргументы, которые есть в некоторых других языках программирования. Но в C++ именованных аргументов точно не будет. А вот designated initializers уже есть...

[prog.angriness] Простите, я о наболевшем на наглядном примере

Не будет большим преувеличением сказать, что каждый раз, когда мы рассказывали на профильных форумах о своих OpenSource разработках, то основной реакций было "да вы сделали никому не нужное говно". Складывалось ощущение, что вокруг они супер-пупер-монстры от программирования, которые могут написать что угодно за пару-тройку дней не отвлекаясь от LOR-овских или RSDN-овских срачей.

Но, блин, если все вокруг такие крутые спецы, которые могут на коленке слабать альтернативу SObjectizer-у или RESTinio, да еще и в 100 раз лучше, то откуда в реальной жизни возникают вопросы типа вот этого: как оптимизировать с++ код, чтобы 7000 бинарников не выедали всё cpu?

Вопрос риторический.

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

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

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

четверг, 15 сентября 2022 г.

[prog.c++] Любопытная задачка с RSDN про гарантии вызова реализации виртуального метода из базового класса

На RSDN-е задали любопытный вопрос. Я этот вопрос понял так:

У нас есть базовый класс A с неким виртуальным методом f. Пользователь создает наследников класса A, в которых он переопределяет f. Но нужно гарантировать, что в переопределенном f обязательно вызывается f из A.

Грубо говоря, вот так нормально:

class A {
public:
   virtual void f() { std::cout << "A::f()" << std::endl; }
};

class B : public A {
public:
   void f() override {
      A::f();
      std::cout << "B::f()" << std::endl;
   }
};

а вот так уже нет, за такое нужно бить по рукам:

class A {
public:
   virtual void f() { std::cout << "A::f()" << std::endl; }
};

class C : public A {
public:
   void f() override {
      std::cout << "C::f()" << std::endl;
   }
};

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

понедельник, 12 сентября 2022 г.

[soft.dev.wtf] Это написано по-русски или по-английски, но русскими буквами?

Отличный абзац в статье на русском языке от представителя российской компании:

Каждой фича-команде завели папки в регресс- и смок-сьютах, а в них уже поместили необходимые кейсы. Ну а чтобы в красивый чистый регресс-набор не попадали драфт-кейсы, завели отдельное пространство с кодовым названием “Кандидаты в регресс”. Некий аналог develop-ветки в git’е. Кейсы лежат там до тех пор, пока не пройдут ревью, и не откроется фича-флаг в новом релизе приложения.

цинк.

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

...но какое-то чувство меры таки нужно иметь. Это необходимо, чтобы текст на русском языке оставался именно текстом на русском языке. А не вот это самое.

С другой стороны, я уже старый, да и сильно оторванный от современных подходов к разработке ПО, особенно молодыми, динамичными коллективами. Возможно, это мне уже в силу старческого маразма написанное плохопонятно, а 20-летним синьор-КьюАрам все более чем очевидно? :(