понедельник, 11 августа 2025 г.

[prog.c++] Оператор <=> и оператор ==

Надеюсь, многие знают, что в C++20 появилась замечательная штука operator<=>. Стоит его определить, как компилятор на его основе выводит другие операторы сравнения, вроде operator< или operator>=.

Причем, что особенно хорошо, можно попросить компилятор сгенерировать operator<=> для нас автоматически:

struct example {
  ... // Какие-то поля.

  auto operator<=>(const example &) const = default;
};

И все. Остальное за нас сделает компилятор.

Однако, думаю, не все знают, что если нам не подходит штатная реализация и мы пишем operator<=> сами, то компилятор потребует, чтобы мы еще и определили свой собственный operator==.

Например, допустим, что у нас в example три поля, а в сравнении должно участвовать только два из них:

struct example {
  int m_a;
  int m_b;
  int m_c;

  auto operator<=>(const example & o) const noexcept
  {
    return std::tie(m_a, m_c) <=> std::tie(o.m_a, o.m_c);
  }
};

В этом случае мы внезапно© обнаружим, что у нас нет operator==.

Таков путь: если мы определили свой operator<=>, то компилятор не будет автоматически выводить равенство и неравенство, а потребует, чтобы мы предоставили operator==.

Может быть вам будет интересно почему?

Если да, то лучше всего прочитать раздел с мотивацией из предложения P1185 и вот этот ответ на Stackoverflow. В двух же словах: реализация сравнения на строгое равенство на базе operator<=> может быть весьма неэффективна.

Например, представим, что мы сравниваем две строки: s1="AA" и s2="AAA".

При лексикографическом сравнении s1 меньше, чем s2: общая подстрока в s1 и s2 совпадает, но в s1 символов меньше.

Когда мы для наших строк s1 и s2 вызываем operator<=>, то этот оператор пытается выяснить как именно соотносятся s1 и s2: меньше ли s1, чем s2, или же s1 больше, чем s2, или же они равны.

Теперь допустим, что в s1 находится миллион символов "A", а в s2 -- миллион и один символ "A". При вызове operator<=> придется сравнить миллион символов в общей подстроке прежде чем мы сможем понять, что s1 все-таки меньше, т.к. у нее символов меньше.

При этом мы не можем прервать сравнение раньше. Допустим, что в s1 будет девятьсот девяносто девять тысяч и девятьсот девяносто девять символов "A", а за ними единственный символ "B", тогда как в s2 все миллион + один символ -- это "A". В таком случае строка s1 окажется больше, не смотря на то, что символов в ней меньше.

Однако, если нам не нужно выяснять общее соотношение между s1 и s2, а достачно просто понять равны ли они или нет, то мы можем начать со сравения длин и лишь затем переходить к посимвольному сравнению.

Когда компилятор по нашей просьбе сам генерирует operator<=>, то он эти фокусы учитывает (под катом небольшой тестовый бенчмарк, который позволяет убедится, что сравнение двух неравных векторов требует минимального времени). Однако, если operator<=> предоставляет пользователь, то у компилятора нет уверенности в том, что пользователь написал свой operator<=> с учетом возможностей оптимизации операции "строгое равенство". И просит нас взять ответственность за operator== на себя.

среда, 6 августа 2025 г.

[prog.c++.thoughts] Design By Contract и приватные методы классов в C++

В C++26 будут включены контракты. Т.е. можно будет сказать, что элементы Design By Contract наконец-то доберутся и до C++.

Недавно в LinkedIn запостил маленький скриншот с примером того, что мне иногда приходится делать посредством комментариев и что будет нормальным образом выражаться в C++26.

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

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


Технология Design By Contract (DbC) берет свое начало в языке Eiffel (вот еще одно описание от первоисточника). Если вы не знакомы с Eiffel хотя бы в части понимания DbC, то вы, к сожалению, многое упустили в своей профессиональной подготовке. Если же знакомы, то, надеюсь, понимать написанное ниже будет проще.

Ключевыми для нашего разговора будут три вещи (на самом деле в Eiffel есть еще и инварианты для циклов, и утверждения check, похожие на C-шный assert, но их мы спокойно можем игнорировать):

вторник, 5 августа 2025 г.

[prog.c++] Forward declarations для типов хорошо бы держать под контролем и в одном месте

С давних пор с настороженностью отношусь к опережающим объявления (forward declarations) типов в коде. Когда вижу что-то вроде:

class A;
class B;
class C {
  const A * _a;
  B & _b;
  ...
};

то невольно начинаю ждать неприятных приключений. Коих, по моим наблюдениям, бывает два вида.

Во-первых, некоторые компиляторы (не будем показывать пальцем в сторону VC++) различают:

class A;

и

struct A;

Поэтому если в каком-то заголовочном файле есть что-то вроде:

class A;
void f(const A &);

А затем в файле реализации:

struct A { ... };
void f(const A & a) { ... };

то при линковке нужная версия f может и не обнаружиться.

Во-вторых, может оказаться, что где-то осталось:

class A;

Тогда как основное определение A было преобразовано в:

template<typename T>
class SomeTrickyTemplate { ... };

using A = SomeTrickyTemplate<int>;

И мы опять же получим приключения при линковке.


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

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

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

пятница, 1 августа 2025 г.

[prog.c++.bugs] Пример типичной C++ной ошибки

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

Вся суть вот в этом фрагменте:

class lock_getter
{
   const std::chrono::steady_clock::duration & m_wait_time_limit;
   ...
   std::condition_variable m_wakeup_cv;
   ...
   bool m_access_granted;

public:
   lock_getter(
      ...,
      std::chono::steady_clock::duration wait_time_limit,
      ...)
      : ...
      , m_wait_time_limit{ wait_time_limit }
      , ...
   {}
   ...
private:
   void try_acquire_or_wait()
   {
      ...
      m_access_granted = false;
      m_wakeup_cv.wait_for(m_lock, m_wait_time_limit,
            [this]() { return m_access_granted; });
      if(!m_access_granted)
         throw std::runtime_error{ "lock can't be acquired" };
      ...
   }
};

Нить A пыталась захватить некий ресурс, который ей по запросу должна была отдать нить B. В ожиданнии подтверждения нить A засыпала, как раз в методе try_acquire_or_wait. Нить B точно разрешала нити A захват ресурса и вызывала для m_wakeup_cv метод notify_one (т.е. точно будила нить A). Но проснувшись нить A почему-то считала, что ресурс ей не дали и порождала исключение. Хотя ресурс ей дали. Но нить A все равно считала, что нет, и бросала исключения.

В общем-то, вся разгадка уже на экране. Нужно только внимательно посмотреть :)

Кому лень смотреть, милости прошу под кат.

[life.cinema] Очередной кинообзор (2025/07)

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

Фильмы

Агент под прикрытием (La infiltrada, 2024). Показалось, что этот фильм должен быть более интересен испанской аудитории, чем международной, т.к. там рассказывается о проблеме сепаратизма басков. Мне же не хватило экшена и напряжения в картине.

Кирпич (Brick, 2025). Смотреть интересно, хотя было несколько моментов, в которых хочется спросить "ну что это за фигня, а?"

М3ГАН (M3GAN, 2022). Местами возникают вопросы из категории "что это, блин, такое было?", но в целом очень бодренько. Так что пойдет чтобы отключить мозги и скоротать вечер.

Апокалипсис Z: Начало конца (Apocalipsis Z: El principio del fin, 2024). Ничего хорошего от фильма не ждал, поэтому был приятно удивлен. Бюджетно, но эта бюджетность играет фильму в плюс, т.к. показывает как будет выглядеть зомби апокалипсис не в Лондоне, Нью-Йорке, Токио или в каком-нибудь другом мегаполисе, а в условном Зажопинске.

Бессмертная гвардия 2 (The Old Guard 2). Если первый фильм понравился, то можно смотреть и второй -- увидите все тоже самое. А если первая часть вызывала рвотный рефлекс, то лучше пройти мимо.

М3ГАН 2.0 (M3GAN 2.0, 2025). Если первый фильм еще пытался быть похожим если не на фильм ужасов, то на триллер, то вторая часть -- это, в лучшем случае, откровенный стеб и тупая подростковая комедия. Вероятно, для компании молодых людей под пиво, зайдет. Но мне не понравилось.

Балерина (Ballerina, 2025). Тупое мочилово ради тупого мочилова. Местами настолько невероятное, что фильм хочется переквалифицировать из "боевика" в "фэнтези".

28 лет спустя (28 Years Later, 2025). Редкостное говно. Неудобоваримая смесь психоделики и маразма. Жаль потраченного на просмотр времени.

Сериалы

Киллербот (Murderbot, первый сезон, 2025). Мне понравилось, хорошая комедия. А вот моей жене -- нет. Так что рекомендовать к просмотру не буду, т.к. может быть эта картина рассчитана на очень специфическую аудиторию, вроде меня.

Роковая афера (Factice, первый сезон, 2025). Странное впечатление. Вроде как замах на незамысловатую криминальную комедию. Но для легкой комедии там слишком много насилия и убийств. Тогда как на полноценную криминальную драму или добротный триллер совершенно не тянет. Тем не менее, на фоне второго сезона "Фишера" выглядит добротно сделанным кино.

Фишер (второй сезон, 2025). Первые три серии были еще ничего, последние три серии -- откровенный шлак, а развязка -- унылое говно. В итоге жалко потраченного времени. Добавлю еще, что на мой взгляд, Ирина Старшенбаум в роли и.о. начальника милиции ну совершенно неубедительна, абсолютный мискаст.

Вне категории

Главы государств (Heads of State, 2025). Это невозможно воспринимать всерьез: авиаперелет из Лондона в итальянский Триест над территорией Белоруссии и эти наши знаменитые горные долины... После такого происходящее на экране превращается в сплошной цирк. Хорошо хоть не уродов, но тут как посмотреть.

четверг, 17 июля 2025 г.

[prog.c++] А ведь в C++ уже давно можно использовать лямбды в качестве локальных функций для упрощения кода...

...думаю я, когда вижу "шедевры" вроде вот такого:

bool data_holder::write_to(const destination & to, const format_version & ver)
{
   if(!m_updated)
      return true;

   std::unique_ptr<abstract_data_writer> writer{
      (ver == version_x_y || ver == version_y_z)
      ? static_cast<abstract_data_writer *>(new legacy_data_writer{to, ver}) : 
      ( (ver == version_z_q)
        ? static_cast<abstract_data_writer *>(new modern_data_writer{to}) :
          throw unsupported_format_version{ver} )
   };

   writer->write_meta_data(...);
   writer->write_headers(...);
   writer->write_comments(...);
   const bool result = writer->complete();

   m_updated = !result;
   return result;
}

Расшифровывать многоэтажное выражение с вложенными тернарными операторами, как по мне, то еще удовольствие. И главный вопрос, который у меня возникает при взгляде на этот код: а почему было не использовать здесь лямбду в качестве локальной функции?

Например, вот так:

bool data_holder::write_to(const destination & to, const format_version & ver)
{
   if(!m_updated)
      return true;

    auto writer = [&]() -> std::unique_ptr<abstract_data_writer>
    {
      if(ver == version_x_y || ver == version_y_z)
         return std::make_unique<legacy_data_writer>(to, ver);
      else if(ver == version_z_q)
         return std::make_unique<modern_data_writer>(to);
      throw unsupported_format_version{ver};
   }();

   writer->write_meta_data(...);
   writer->write_headers(...);
   writer->write_comments(...);
   const bool result = writer->complete();

   m_updated = !result;
   return result;
}

Здесь и вникнуть в суть происходящего проще, да и расширить со временем новым типом writer-а будет проще.

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

bool data_holder::write_to(const destination & to, const format_version & ver)
{
   if(!m_updated)
      return true;

   const auto do_write = [this](abstract_data_writer & writer) {
      writer.write_meta_data(...);
      writer.write_headers(...);
      writer.write_comments(...);
      const bool result = writer.complete();

      m_updated = !result;
      return result;
   };

   if(ver == version_x_y || ver == version_y_z)
   {
      legacy_data_writer writer{to, ver};
      return do_write(writer);
   }
   else if(ver == version_z_q)
   {
      modern_data_writer writer{to};
      return do_write(writer);
   }

   throw unsupported_format_version{ver};
}

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

PS. Пожалуйста, не спрашивайте почему в оригинальном коде используется static_cast (а он там и не нужен). У меня нет цензурного ответа.

суббота, 12 июля 2025 г.

[prog.c++] Примеры нововведений в С++, которые "осчастливили" меня (на самом деле нет)

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

Хочу перечислить здесь несколько случаев когда в стандарт C++ вносили нечто, что вызывает у меня неоднозначные чувства, а иногда и прямо-таки негативные. ИМХО, в каждом из этих случаев можно было либо вообще отказаться от нововведений, либо же сделать их иначе.

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

Ладно, хватит предисловий, двайте начнем.


std::launder. Очень специфическая штука, которую мало кто понимает. И, что раздражает больше всего, ведь до введения std::launder в стандарт код у людей работал. И сейчас у многих работает. Но может внезапно перестать. При этом если опросить 1000 случайных C++ разработчиков, то, боюсь, малый процент из них вообще сможет внятно рассказать для чего std::launder в принципе нужен. И еще меньший процент сможет объяснить где же std::launder должен применяться, а где нет.

При этом std::launder имеет отношение к такой штуке как lifetime, но когда вводили std::launder почему-то не включили в стандарт никаких других инструментов для объявления начала lifetime в остальных случаях, не покрываемых std::launder. Например, абсолютно необходимая во многих ситуациях std::start_lifetime_as была добавлена только в C++23. При том, что многие проекты все еще на С++17 и непонятно когда на C++23 смогут перейти.

Т.е. в C++17 разработчиков поставили перед фактом о том, что теперь компилятор может начать эксплуатировать UB, связанные с lifetime-ами. И лишь в C++23 программистам дали нормальный набор инструментов.

При этом в C++20 сделали некую полумеру -- объявили набор магических функций и ситуаций, когда lifetime неявно начинается. Типа std::malloc начинает lifetime, а какая-то произвольная пользовательская функция, скажем, my::shared_memory_arena::malloc -- нет, потому что стандарт про нее не знает, и стандарт не дает никаких возможностей указать, что пользовательская функция ведет себя как malloc.

Если смотреть на всю эту эпопею с std::launder в C++17, implicit lifetimes в C++20 и std::start_lifetime_as в C++23, то складывается ощущение, что люди из комитета тупо затыкали дыры по мере их обнаружения. Вместо того, чтобы провести глубокий анализ и выкатить полноценное решение, а не набор отдельных "заплаток", да еще и растянутых на период в шесть лет.

Хотя, как по мне, так все эти заморочки с lifetime-ами нужно вообще выбросить. И если эта штука так важна разработчикам компиляторов, то пусть с ней разбираются разработчики компиляторов, а не обычные C++ программисты, которым и так граблей насыпают с каждым стандартом.


Модули.

Тут мне вообще сложно говорить цензурно.

Само появление модулей в стандарте означает что все, кто на свою беду сделал собственные системы сборки С++ных проектов, теперь вынуждены будут либо допиливать их до поддержки модулей, либо же менять на что-то (а что-то -- это скорее всего CMake).

Кроме того, есть у меня ощущение, что модули не дадут заметного положительного эффекта в случаях, когда в коде используется много шаблонов. Ибо когда сейчас имеешь дело с таким кодом, то даже невооруженным взглядом видно, что основное время тратится не на парсинг C++ного кода, а на инстанцирование шаблонов и их оптимизацию. Т.е. если у меня сейчас .cpp-файл транслируется 20 секунд, включая все include и пр. расходы от которых типа должны освободить модули, то с модулями этот файл будет транслироваться 19 или, если поведет, 18 секунд. Отличное достижение, прям таки победа.

Может быть какой-то выигрыш от модулей был заметен в 2019-ом на тогдашних CPU и HDD-дисках. Сейчас, когда даже в бюджетных ноутбуках стоят CPU с десятком(!) ядер и SSD со скоростями больше нескольких гигабайт в секунду, проблем с длительной обработкой большого количества include нет от слова совсем.

Ну и главное: сейчас уже середина 2025-го года, а нормальной полноценной поддержки модулей в большой тройке компиляторов нет.

И спрашивается: а оно вообще стоило того? Угробить кучу ресурсов на внедрение в стандарт, компиляторы, IDE и пр. инструменты поддержку явно переусложненной системы модулей, которая вносит в C++ такой же раскол, как Python3 по отношению к Python2. И не получить вменяемого результата даже через пять лет после выхода С++20.

Это, блин, полный успех.

Попробовали бы в какой-то коммерческой разработке так долго "скармливать" заказчику появление подобной "супер-мега-киллер-фичи". А над обычными C++никами можно. Раз уж мы решили оставаться в этом языке, то нужно страдать и ждать обещанного счастья по 5 лет.


Неоднократно поминаемый мной deducing this. Не буду много распространяться, просто дам ссылку на свой же пост двухлетней давности: тыц.

Что меня раздражает в deducing this больше всего, так это не то, что в C++ добавили очередной всратый синтаксис для того, чтобы сделать тоже самое, но совсем другим способом, а то, что deducing this вообще не дружит с DLL/SO библиотеками.

Вот хотите вы экспортировать из DLL/SO свой C++ный класс. И в этом классе есть дублирующиеся методы, количество которых хотелось бы подсократить посредством deducing this. Но вы этого не сможете, потому что deducing this работает по принципу C++ных шаблонов.

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

Идиотия чистой воды. Но, видимо, кто с Python-измом или Rust-оизмом головного мозга очень хотел превратить C++ в подобие Python/Rust с явной передачей self-а в методы класса. И у этого "кого-то", что характерно, получилось 🙁


Включенная в C++26 рефлексия времени компиляции.

Тут я нахожусь в полной растерянности от выбранного синтаксиса.

Во-первых, каким боком ^^ к рефлексии? Ведь в C++ есть оператор ^, как этот оператор соотносится с рефлексией? Никак? Здесь нет логики, нужно просто запомнить, что ^ -- это про операции над битами (если он не перегружен, конечно), а ^^ -- это рефлексия? Ну так чем ^^ лучше любого другого единичного символа? Почему ^^T, а не @T или %T или даже `T?

Во-вторых, почему "операторные скобки" для рефлексии используют двоеточие? Оно же тупо плохо заметно в тексте в комбинациях [: и :]. И, кроме того, оно вообще никак не соотносится с ^^.

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

Логично и последовательно, да.


В общем, что хочу сказать в завершение...

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

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

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

Ну и да, комитету насрать на то, когда приятое им в стандарт появится в большинстве компиляторов в приемлемом качестве. Поэтому давайте штамповать фичи, которые доберутся до рядового разработчика через 5-6-7 лет. А то и не доберутся вовсе, как export template из С++98. Пофиг, пипл схавает.

Поэтому еще раз повторю то, что уже писал в разных соцсетях:

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