Pull to refresh
25
0
Александр Дербенёв @alexac

Senior Software Engineer

Send message

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

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

Но это ладно. Меня больше задевает обработка ошибок исключительно через исключения. При том, что куча крупных проектов и почти весь embedded мир намеренно их выключают (see llvm, chromium, google c++ code convention, etc.)

Главное уметь. Для того чтобы сделать установочную флешку из macOS достаточно скачанного образа, disk utility и wimlib, которую можно установить из homebrew. Ставил винду на ноут сестры месяц назад.

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

std::string ApplySpell(Spell *spell) {
  using namespace std::placeholders;
  return Maybe(spell).Or("No spell")
    .Filter(std::bind(&Spell::IsValid, _1)).Or("Invalid spell")
    .FilterOut(std::bind(&Self::IsImuneToSpell, this, _1)).Or("Immune to spell")
    .FilterOut(std::bind(&Self::IsSpellApplied, this, _1)).Or("Spell already applied")
    .Then([](Spell* spell) -> std::string {
      applied_spells_.Append(spell);
      ApplyEffects(spell->GetEffects());
      return"Spell applied";
    });
}

Другой вопрос, стоит ли оно того? Может лучше декомпозировать функцию на отдельно проверки и полезную нагрузку? Да и использовать менее развесистое форматирование для ветвлений?

std::string ApplySpell(Spell *spell) {
  return ValidateSpell(spell)
      .OrElse(std::bind(&Self::ApplySpellImpl, this, std::ref(*spell)));
}

private:
Maybe<std::string> ValidateSpell(Spell* spell) {
  if (!spell) { return "No spell"; }
  if (!spell->IsValid()) { return "Invalid spell"; }
  if (IsImmuneToSpell(*spell)) { return "Immune to spell"; }
  if (IsSpellApplied(*this)) { return "Spell already applied"; }

  return {};
}

std::string ApplySpellImpl(Spell& spell) {
  applied_spells_.Append(spell);
  ApplyEffects(spell.GetEffects());
  return "Spell applied";
}

Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно. Но это очень большие изменения в языке, поэтому я даже не надеюсь на это. Ну и да, compile-time reflection, было бы прекрасно.

Внезапно, да. Какая разница, в каком порядке цифры, если набираешь все равно вслепую, механически зная как и что надо нажать для нужного символа? На то чтобы привыкнуть ушло какое-то время, да. Единственное неудобство, которое осталось — при переключении на любую другую раскладку цифры немного ломают мозг, конечно.

Попробуйте programmer Dvorak? Там специально вся пунктуация убрана с шифт-слоя в нормальный, а цифры наоборот вынесены в шифт слой и идут в том же порядке, что в оригинальной раскладке Дворака ( идея в том, что при программировании всевозможные символы пунктуации гораздо нужнее, чем цифры, которые нужны только для значений констант). Использую эту раскладку уже 15 лет, мне удобно.

Если использовать clang, то есть удобный экстеншн (https://clang.llvm.org/docs/LanguageExtensions.html#vectors-and-extended-vectors), который позволяет объявлять векторные типы с такой же семантикой, как в OpenCL. Именно этот экстеншн эппл использует, чтобы имлементировать векторные типы в simd фреймворке.

Работает через typedef/using с аттрибутом ext_vector_type(N):

#if __has_attribute(ext_vector_type)
using float2 = float __attribute__((ext_vector_type(2)));
using float3 = float __attribute__((ext_vector_type(3)));
using float4 = float __attribute__((ext_vector_type(4)));

using int2 = int __attribute__((ext_vector_type(2)));
using int3 = int __attribute__((ext_vector_type(3)));
using int4 = int __attribute__((ext_vector_type(4)));
#endif

Да, это не стандартное поведение а специфичный для компилятора экстеншн, но зато работает без магии на макросах, да и вовсе без какого-либо дополнительного кода. И транслируется в нативные simd инструкции автоматически.

У gcc есть подобный экстеншн и аттрибут для объявления векторных типов, но swizzle там не поддерживается.

  1. по факту подавляющее большинство современных компиляторов поддерживают #pragma once без проблем. Но например у gcc и clang есть предупреждение про использование в .cpp файле, которое, к тому же включено по умолчанию:

alex-ac@alex-ac-mbp /tmp $ echo '#pragma once' >test.cc
alex-ac@alex-ac-mbp /tmp $ clang++ -c test.cc
test.cc:1:9: warning: #pragma once in main file [-Wpragma-once-outside-header]
#pragma once
        ^
1 warning generated.
  1. да, согласен, но не бездумно везде. На самом деле эта рекомендация может уйти прошлое с модулями. Главное чтобы никто не начал писать export using namespace std;. И вообще, какой-нибудь using namespace std::placeholders; внутри функции бывает очень полезен.

  2. Элементарно же. #define - просто подставляет текст. нет информации о типах, есть вероятность, что будет парситься не так, как этого ожидаешь если рядом какие-то сложные выражения. Объявление константы добавляет сущность, которая имеет явный тип, на которую распространяется ODR, на которую зарезервировано место в бинарнике, etc. Гораздо меньше вариантов выстрелить себе в ногу. Есть два повода писать #define в современном C++ - если объявленное значение нужно использовать в #if, или если объявляется макрос с параметрами, который позволяет значительно сократить написание повторяющегося кода. Причем, последнее спорно. В целом, чем дальше развивается язык, тем меньше становится потребность в препроцессоре.

  3. Да, глобальные переменные ок для некоторых задач. Но для общего назначения это антипаттерн.

  4. Все ради инкапсуляции. Все, что находится в приватной секции не является интерфейсом класса (за исключением того, как это влияет на размер объекта и vtable). Декларируя в публичной секции поля мы декларируем эти поля частью интерфейса класса и лишаем себя возможности инкапсулировать логику связанную с изменением значений этих полей. Когда мы прячем поля в приватной секции и добавляем к публичному интерфейсу геттер/сеттер, мы оставляем возможность инкапсулировать любую логику в сеттер или делегировать хранение значение в какой-то другой объект/объекты. В Visual C++, objective-c, swift, C# и JS есть property, которые позволяют сделать property частью публичного интерфейса, при этом оставляя возможность инкапсулировать логику по изменению их значений (и по факту являются синтаксическим сахаром для гет/сеттеров. Но в стандартном C++ этого нет, поэтому оправдано убирать все в приватную секцию всегда. И да, если нет необходимости в инкапсуляции данных, возможно действительно нужны структуры, которые сейчас, по факту, — те же классы, имеющие только публичную секцию.

  5. Ну вот да, в современном C++ видеть new/delete без реальной на то необходимости — плохо. Аллокация на стеке — гораздо лучше. Если надо в куче — make_unique/make_shared, пожалуйста.

  6. Нет. Во-первых, есть компиляторы, которые на такое ругаются при достаточно параноидальных настройках. Во-вторых, многие билд-системы будут делать предположение, что .cpp файл — это отдельная единица трансляции, которая должна быть скомпилирована в объектник и добавлена на этапе линковки. Включение целиком одного .cpp файла в другой приведет к тому, что все символы в этом файле будут объявлены в двух единицах трансляции. И если для какой-то части из них это не имеет значения благодаря ODR, то в большинстве случаев это приведет к ошибкам линковки этих символов. По крайней мере к предупреждениям о том, что это случилось и линковщику пришлось выбирать между ними.

Я намеренно не хотел расписывать подробно по каждому из пунктов, почему это так. У автора явно все слишком плохо с знанием языка, чтобы понимать причины и концепции на которых основаны эти объяснения, а те, кто знают язык и без объяснений про это знают. Моей целью было указать на ошибки, чтобы автор сам изучил эти вопросы и усвоил это лучше, чем если бы ему сказали ответ, который он все равно до конца не понимает.

На голом WinAPI тоже пишут, но весьма не много, и как правило когда редактируют куски какого-либо фреймворка, который на нижнем уровне все равно будет иметь доступ к WinAPI.

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

Ты прав в том, что в сущности программирование — композиция абстракций от элементарного уровня до очень сложного. Однако это не все, что нужно понимать. Еще очень важно понимать и знать общепринятые паттерны, алгоритмы и структуры данных, которые повсеместно используются для этой композиции. Инженер при работе не думает о том, как решать простые задачи, он знает типовые решения и применяет их. И другой инженер, читая код спустя пол года, с пары взглядов узнает паттерны и понимает, что делает этот код. Не менее важно знать инструменты, которые ты используешь. C++ не тот язык, который можно освоить прочитав одну статью за пол часа. Уходят месяцы и годы, чтобы понять большинство его аспектов.

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

Некоторые из ошибок которые бросились в глаза:

  1. #pragma once - стоит выучить, что это и зачем нужно. И почему это не нужно добавлять в .cpp файлы (хорошие компиляторы с правильными настройками напишут предупреждение про это).

  2. using namespace std - плохая привычка. И нет, это совсем не другой способ импорта.

  3. #define - очень плохой с точки зрения C++ способ объявлять константы.

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

  5. Есть веские причины, почему практически всегда свойства класса помещают в private: секцию, или по крайней мере в protected:. Если этого не нужно, возможно не нужен сам класс.

  6. Любому new должен соответствовать delete, а в современном C++ ни того, ни другого присутствовать не должно. Есть умные указатели, которые помогают управлять памятью.

  7. Использовать #include на .cpp файлах — в корне не правильно и приводит к ошибкам линковки.

В хроме для этого есть профили, но они в разных окнах, не табах.

Линковаться с GPL библиотекой нельзя, не публикуя свой код под GPL. Даже если не распространяете GPL-код вместе с продуктом. Как раз распространять код вместе GPL не запрещает. Линковка же весьма спорный момент, но по факту большинство разночтений сводятся к тому, что линковка к библиотеке подразумевает включение ее логики в программу, и тогда вся программа должна быть опубликована под GPL. Есть LGPL, которая разрешает линковку к LGPL библиотеке без публикации кода остальной программы. А еще есть AGPL - самая лютая хрень, с ней даже обернуть в отдельный сервис, который доступен по сети, нельзя, не выкладывая весь свой код под AGPL.

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

А собственно, что там невозможного, написать кастомный делитер?

#include <memory>

struct StringBuilderDeleter {
  operator()(StringBuilder* ptr) const {
    DestroyStringBuilder(ptr);
  }
};

using StringBuilderPtr = std::unique_ptr<StringBuilder, StringBuilderDeleter>;

Разумеется, они ставят одну куку на корень, но когда у тебя все домены в разных tld, приходится выкручиваться.

yandex.net
yandex.ru
yandex.com
yandex.com.tr
yandex.ua
ya.ru

список можно продолжать долго, но думаю суть понятна.

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

Мы не дошли до оптимизации сборки кэшами. Команда решила перенять процессы/инструменты у соседней команды, которая начала работу заметно раньше, и вместе с этим притащили базел. Практически сразу я понял, что все кроме меня воспринимают конфигурацию базеля как магию и не понимают, что там происходит. Вся соседняя команда работала на amd64 и им кросс-компиляция нахер не сдалась. А у нас вся команда работает на arm64 и нам бы сборку под две архитектуры поддерживать. А поддерживать такую сложную штуку просто ради того, чтобы она была (потому что опять же никаких плюсов по сравнению с родным тулчейном go), нафиг не надо. Ну и у нас у каждого отдельно взятого микросервиса сборка/тесты проходят меньше чем за минуту. Нам просто нет нужды пытаться это оптимизировать. С базелем без кэшей оно становится втрое дольше, а настраивать кэши базеля для того чтобы решить проблему которую использование базеля и создало...

Кросс-компиляция с CGO это ад даже в самом Go, я недавно заставлял это работать с одним из сервисов.

Я в своем проекте в какой-то момент сделал обратную вещь и выбросил bazel в пользу родного тулинга go/docker и обернул это все мэйкфайлами. По факту, мы поняли, что для нашего проекта bazel не решает ни одной задачи, которые решает родной тулинг. Зато добавляет геморой с поддержкой конфигурации сборки и скорее замедляет сборки (у базеля есть заметный оверхед на настройку тулчейна, который на сборке мелких программ в CI очень заметен). В добавок, мы хотели мультиархитектурные образы на выходе (arm64/amd64), а значит было нужно, чтобы у нас работала нормально кросс-компиляция и создание манифеста в конце. А кросс-компиляция в базеле отвратительно работает.

Зануда мод: UART требует делителя Частот, настройки скорости передачи, настройки наличия и количества старт/стоп битов, настройки проверки на четность, etc. SPI передает тактовый сигнал от мастера к слейву синхронно с данными, с опциональной адресацией слейвов по chip-select, а потому весь интерфейс SPI на слейве состоит из двух сдвиговых регистров и двух регистров буфферов для синхронизации с тактовой частотой контроллера. Самый простой драйвер UART всегда будет сложнее, чем самый простой драйвер SPI-slave.

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

Формат Mach-O достаточно хорошо документирован, а инструменты для работы с ним опенсорсные и кроссплатформенные. Для сборки под macos/ios нужны файлы из SDK, который можно легко вытащить из xcode, но в остальном, если не использовать interface-builder, то ничего не мешает собирать код на других платформах. Если писать на go/rust, это уже работает из коробки, если писать на C/C++/Objective-C/Objective-C++, то с использованием llvm это потребует каких-то усилий на настройку, но в принципе тоже реализуемо.

Плюс есть особые кейсы вроде игровых движков, когда создатель движка собирает код движка в бинарник, а для сборки игры нужно просто сложить вместе бинарники движка и архивы со скриптами и прочими ресурсами. А потом подписать. Например так устроена сборка игр на defold. Инструмент сборки там отлично справляется над тем, чтобы собрать приложение под mac на linux. А вот под ios отказывается, так как хочет работать с подписью.

Есть смысл, на самом деле. C - это lingua franca. Когда нужно дружить друг с другом код на разных языках, зачастую все сводится к предоставлению C-интерфейса. И может быть в имплементации этого интерфейса будет уже C++, go, rust, ассемблере или вообще чем-то экзотичном. Но интерфейс будет на C потому что подцепиться к нему можно из максимально широкого круга языков. Писать весь проект на C, мало кто будет, но какие-то части регулярно получаются написанными на C или хотя бы представляют из себя интерфейсы на C.

Information

Rating
Does not participate
Date of birth
Registered
Activity

Specialization

Software Developer
Senior
From 100,000 €