C++ Embedded
424 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
barbie-sized array
Даже в условиях мировой нестабильности одно утверждение остается незыблемым: std::array лучше простецкого сишного массива. Хотя бы потому, что не нужно голову ломать над проблемой нулевого размера. Стандарт уже обязывает array учитывать такую возможность. Заглянем в раздел 24.3.7.5 Zero-sized arrays [array.zero]:
- std::array обязан обеспечивать поддержку особого случая при N == 0, где N - размер массива.
- Если N == 0, то методы begin() и end() должны вернуть одинаковое уникальное значение. Что вернет метод data(), никто не знает.
- Что может случиться при вызове front() или back() непонятно.
- Метод swap() не должен бросаться исключениями.
Подсмотрим одну из стандартных реализаций класса array:
template<typename _Tp, std::size_t _Nm>
struct array {
...
// Support for zero-sized arrays mandatory.
typedef __array_traits<_Tp, _Nm> _AT_Type;
typename _AT_Type::_Type _M_elems;

То есть тип внутренностей _M_elems определяется неким __array_traits. Если посмотреть вооруженным глазом, то в общем случае это будет просто массив:
template<typename _Tp, std::size_t _Nm>
struct __array_traits {
typedef _Tp _Type[_Nm];

Однако в угоду стандарту имеется и частичная специализация под нулевой размер:
template<typename _Tp>
struct __array_traits<_Tp, 0> {
struct _Type { };

Вам не показалось, этот класс пуст, как барабан. Благодаря чему реальный размер нулевого std::array будет больше нуля.
static_assert(sizeof(std::array<uin64_t, 0>) == 1);  // OK

А что IAR? Тут тоже имеется своя "классическая" реализация массива, ничего примечательного. Только случай нулевого массива обрабатывается частичной специализацией аж всего класса array, разработчики IAR не размениваются на мелочи.
template<class _Ty>
class array<_Ty, 0> { // zero size array of values
public:
...
_Ty _Elems[1];
};

Вот здесь нас подстерегает неожиданный эффект, массив хоть и нулевого размера, но припрятал себе один элемент. Судя по всему, он понадобился ради "уникального значения", что по стандарту обязаны вернуть begin() и end(), да и front() и back() должны что-то выдавать. Хотя может быть не очень приятно получить объект там, где не планировали.
static_assert(sizeof(std::array<uin64_t, 0>) == sizeof(std::array<uin64_t, 1>));  // OK

Немного лучше дела обстоят в libcpp библиотеке IAR. Спасибо уже за то, что никакой дополнительный объект не создается, но приглядимся внимательней к реализации:
template <class _Tp>
struct array<_Tp, 0> {
...
typedef typename conditional<is_const<_Tp>::value, const char, char>::type _CharType;
struct _ArrayInStructT { _Tp __data_[1]; };
_ALIGNAS_TYPE(_ArrayInStructT) _CharType __elems_[sizeof(_ArrayInStructT)];

это означает, что создается выровненный массив байтов, который готов стать элементом. Вот только он нигде не востребован. Видимо, оставили для совместимости. Благодаря этому нулевой array в IAR всегда будет равен размеру одного гипотетического элемента.
static_assert(sizeof(std::array<uin64_t, 0>) == 8);  // OK

Можно бесконечно долго наблюдать, как разработчики стандартных библиотек разруливают граничные случаи. Еще лучше присоединиться к этому гордому племени и писать свои контейнеры, дабы на себе ощутить этот хтонический ужас.
👍6
covariant return types
Я часто вижу в коде молодых коллег неуклюжие, нелепые, а иногда и пугающие строчки. Смотреть на такое тяжело, поэтому не очень люблю делать ревью кода. Почти всегда это стресс, а порой просто страх. Тем паче, древняя китайская мудрость гласит: "Тщательное ревью - к ссоре". Вот вчера приносит мой коллега Мартын мне кусок крайне важных изменений и сквозь зубы умоляет посмотреть. Мне Мартын не очень нравится, поэтому я начинаю рассматривать код под микроскопом:
struct Base {};
struct Derived : Base {};

struct B {
virtual ~B() = default;
virtual Base *f(int) { ... }
};
struct D : B {
~D() override = default;
Derived *f(int) override { ... }
};

Тут я и усомнился в том, что действительно преисполнился в познании плюсов. Перегруженный метод возвращает пусть и родственный, но иной тип?
"О, это же классические ковариантные возвращаемые типы!" - восклицает за спиной уборщик, совершенно случайно проходящий мимо. - "Тысячу раз так делал!"
Я же с ужасом понимаю, что ни разу в жизни не применял ЭТО в проектах. Дрожащими руками набираю странное название в гугле. Еще теплится надежда, что это просто незначительное нововведение. Бессердечная всемирная паутина не оставляет мне шансов. ЭТО было в стандартах всегда с 1998 года! Есть даже одноименная идиома для объектно-ориентированных языков программирования, которую плюсы реализуют "из коробки".
Уверен, все мы на животном уровне понимаем, что такое вполне возможно, но лучше сверимся со стандартом. В разделе 11.7.3 Virtual functions [class.virtual] стих 8, содержится исчерпывающая информация об удивительном явлении:
Возвращаемый тип перегруженной функции должен быть идентичен или ковариантен возвращаемому типу перегружаемой функции.
Если функция D::f перегружает функцию B::f, то возвращаемые типы ковариантны, если они удовлетворяют следующим критериям:
- оба типа - это указатели или ссылки (lvalue или rvalue) на классы;
- класс в возвращаемом типе B::f это тот же класс, что и в возвращаемом типе D::f или же однозначный и доступный базовый класс в возвращаемом типе D::f;
- указатели или ссылки имеют одинаковую cv-квалификацию и класс в возвращаемом типе D::f имеет такую же или меньшую cv-квалификацию как и класс в возвращаемом типе B::f

Удовлетворяя свое любопытство посмотрим на символы, которые определит компилятор для виртуальных методов B::f(int) и D::f(int):
0x080007c0 _ZN1B1fEi
0x08000840 _ZN1D1fEi

Имена отличатся всего лишь именем класса, зашифрованным внутри. Это нормально, как говорила собака из известного мема.
Как мы хорошо помним, возвращаемый тип не участвует в формировании символических имен методов, если они не шаблонные. А виртуальные методы и не могут быть шаблонными, поэтому замена возвращаемого типа на дочерний несильно повлияет на символическое имя. Вот использование ковариантного параметра могло бы все изменить, но это, слава Страуструпу, запрещено.
Так же не сработает замена обычного скучного указателя на умный. В большинстве своем шаблонные типы инвариантны, то есть мы не можем сделать так:
struct B {
virtual std::unique_ptr<Base> f(int) { ... }
};
struct D : B {
std::unique_ptr<Derived> f(int) override { ... }
};

Осталось только выяснить, где же применяется эта идиома, если за сто лет мне ни разу не понадобилась.
👍3🤔1
why covariant?
Итак, клонирование. Пока британские ученые цепко держат нас на бренной земле многострадальной овечкой Долли, мысль человеческая уносится в глубины космоса, рисуя всем известные сцены из "Звездных войн". Великие гуманитарные мыслители современности уже бьются над только проступающими моральными дилеммами. Этично ли программисту клонировать себя и устроить копию еще на две удаленные работы? Или же завести детей, чтоб к пяти годам научить программировать не хуже бывших португальских коллег? Многие писатели обращались к благодатной теме правильного клонирования. Вот, к примеру, Кир Булычев в повести "Чужая память" утверждал устами главного героя: "...делать людей дешевле ортодоксальным путем". Ой, простите, это не тот фрагмент. Та-а-ак, говорил он вот что: "Клонирование - давно не секрет". Тут великий мастер прав, в плюсах есть не идиома, но известный прием: клонирование сквозь интерфейс.
Как скопировать объект? Тривиальный вопрос! Можно использовать копирующий конструктор или соответствующий оператор. Что делать, если у нас нет доступа к объекту, а только к его интерфейсу? Тогда просто скопировать не получится, надо копировать сложно:
ObjectInterface &o1; 
Object o2 {*o1->clone()};

Интерфейс должен предоставлять метод клонирования. Иногда так действительно делают в каком-нибудь контейнере, где хранится непонятно что, и доступ к элементам возможен только через интерфейс базового класса. Например, такой:
struct ObjectInterface {
virtual ObjectInterface *clone() = 0;

Тогда метод clone в классе Object должен выглядеть как ObjectInterface *clone() override {}
Однако сейчас мы знаем, что возвращаемые типы могут быть ковариантными, и это может избавить нас от некоторых проблем в будущем: Object *clone() override {}
Не придется делать dynamic_cast<Object *>(o1->clone()), если вдруг мы уверены, что получится тип Object, и немедленно хотим использовать какие-то его специфические методы.
Я долго искал пример, где преимущество применения ковариантных типов было бы очевидно, но не нашел. Пример пришлось надумать: представим, что у нас есть некий класс Ground с целым числом внутри. Этот класс и будет базовым или интерфейсом с методом clone.
struct Ground {
Ground(int32_t const v) : value{v} {}
virtual Ground *clone() { return new Ground{value}; }
void clear() { value = 0; }
int32_t value;
};

Допустим, есть класс-наследник UnderGround. Представим, что он сильно отличается от базового класса, хоть и переиспользует его конструктор
struct UnderGround final : Ground {
using Ground::Ground;
UnderGround *clone() { return new UnderGround{value}; }
void clear() { value = -100; }
};

В придачу нам досталась функция CloneAndClear, которая не только клонирует, но и очищает вновь созданный объект. Эта супер-функция налагает дополнительные ограничения на используемый в качестве аргумента тип: он должен иметь не только предоставляемый Ground виртуальный метод clone, но и метод clear, который уже не принадлежит интерфейсу, но должен быть по-разному реализован для разных классов.
template <class T>
auto CloneAndClear(T &t) {
auto *ptr = t.clone();
ptr->clear();
return *ptr;
}

Приподзакрыв глаза на текущую память и иные проблемы, констатируем, что конструкция работает идеально:
UnderGround x {1};
auto y = CloneAndClear(x); // y.value == -100

Если бы UnderGround::clone вернул тип Ground*, то и вызван был бы метод Ground::clear, и y.value было бы равно не -100, а нулю.
Я говорил, что ситуация надуманная? Значит, она обязательно произойдет в продуктовом коде.
👍2🤔1
Boost mutant
Древние говорили: "Omnia mutabantur, mutantur, mutabuntur". Они думали, что все меняется. Просто их не собеседовали в компании "Кошмарский". За столько лет ничего не поменялось. Такое чувство, что отвратительные интервью даже стали частью корпоративной культуры. Осталось только доверить отбор кандидатов говорящему ослу с распечатанным оглавлением талмуда Таненбаума вместо списка вопросов. Но где же взять такого мутанта? В дикой природе таких не видел, а вот в бустовом коде поймал одного. Занятная зверушка.
В этом случае мутант - это просто структура с измененным порядком членов, но без физической реструктуризации или переноса вещества. Больше похоже на кривое зеркало. Или порно-пародию. Примечательно, что для изготовления видоизмененной сущности нам не потребуется даже мутаген, отобравший былую красоту у Хамато Ёси. Достаточно шаблонов:
template <class Pair>
struct Reverse {
typedef typename Pair::first_type second_type;
typedef typename Pair::second_type first_type;
second_type second;
first_type first;
};

Согласен, структурка уродливая, прямо как Дэдпул. Однако работает! Правда, только с парами, почти как семейный психолог за 2000 рублей в час.
Думаю, вы уже поняли, насколько идея гениальна в своей простоте. Если у нас есть пара:
struct pair {
first_type first;
second_type second;
};

мы можем "поменять местами" элементы вот так:
struct Reverse<pair> {
first_type second;
second_type first;
};

Шаблонная структура Reverse поменяет имена элементов данных без шума, пыли и всякого физического вмешательства.
Если объект std::pair<X,Y> содержит только элементы данных POD (не забудем потом проверить std::is_trivial и std::is_standard_layout), то объект Reverse<std::pair<X,Y>> в памяти абсолютно идентичен оригиналу.
Для доступа к "новым" именам переменных воспользуемся вспомогательной функцией:
template <class Pair>
Reverse<Pair> & mutate(Pair & p) {
return reinterpret_cast<Reverse<Pair> &>(p);
}

Функция mutate цепляет на std::pair маску Reverse: преобразование reinterpret_cast приводит ссылку на Pair к ссылке на новую, хоть и идентичную структуру.
Здесь мы ходим по очень тонкому льду предположения, что две структуры разных типов с таким же набором (по типу и порядку) членов размещаются в памяти одинаково. Хотя стандарт с++ этого не гарантирует, однако все компиляторы поступают разумно и не разочаровывают нас.
Продемонстрируем безупречную работу метаморфа:
std::pair<uint32_t, float> x {42U, 3.14F};
x.first == 42, x.second == 3.140
mutate(x).first == 3.140, mutate(x).second == 42

Учитывая, что пары часто используются в контейнерах, то такие мутации могут добавить им пару веселеньких свойств.
👍4🔥2
bimap
Обидно, когда долго корпишь над какой-нибудь проблемой, а она уже успешно решена десятки раз. Взять хоть к примеру классическую проблему с enum. Неплохо бы по значению получать текстовое описание перечисления, чтоб генерировать удобночитаемый json. И наоборот, по текстовому описанию находить значение, чтоб парсить злополучный json. Вариантов, как это реализовать, масса. Можно в лоб описать пару конструкций switch, но такой подход не слишком гибкий. Можно использовать массив со строками, но значения перечисления могут идти не по порядку. Итого, после нескольких недель страданий и усердного обмозговывания задачи, повлекшего за собой повышенное артериальное давление, я узнаю, что все уже украдено до нас! Уже есть контейнер, хранящий набор пар, где искать можно в двух направлениях: значение по ключу и ключ по значению. Хорошо, если и ключи, и значения уникальны. Названо это ужасным именем bimap, глядя на которое сложно распознать его истинное предназначение. Кажется, что это bitmap, написанное с ошибкой, а не "bidirectional map".
Конечно, этот bimap есть в boost, но не тащить же его в проект. Надо реализовать своими силами. Во-первых, это красиво. Во-вторых, коллеги забавно хватаются за головы, когда видят новые шаблонные штуки в пулл-реквестах.
Итак, я беру чистый заголовочный файл и смело пишу:
template <class Key, class Value>
struct Bimap {

Дальше вдохновение закончилось, и пришлось думать. Контейнер должен уметь искать значение по ключу, это несложно реализовать. Достаточно обойти все элементы и сравнить с искомым значением первый член пары. То же самое с поиском по значению. Однако позволим себе небольшое обобщение:
template <class T, class Ret>
int32_t Find(T key, Ret(*func)(std::pair<Key, Value> &)) {
for (int32_t i{}; i < elements_.size(); i++) {
if (func(elements_[i]).first == key) {
return i;
}
}
return -1;
}

Пусть метод Find будет шаблонным. Первым аргументом будет искомое значение, тип которого можно вывести автоматически. Второй аргумент - это функция преобразования пары. Заметим, что возвращаемое значение - это второй шаблонный тип Ret, который тоже может быть выведен компилятором без подсказки разработчика.
Сделано это не просто так, а чтоб наш дружелюбный сосед-мутант получил работу. В настоящем-то bimap все сделано не так топорно, но и этого хватит, чтоб проиллюстрировать использование одного и того же алгоритма для пары и инвертированной пары.
Для поиска по ключу напишем такой метод:
int32_t FindByKey(Key const key) {
return Find(key, [](std::pair<Key, Value> &p) { return p; });
}

В качестве второго параметра передаем в Find лямбду, которая возвращает тот же самый объект типа std::pair<Key, Value>.
Если нам нужно искать ключ по значению:
int32_t FindByValue(Value const value) {
return Find(value, &mutate<std::pair<Key, Value>>);
}

Передаем в качестве второго параметра знакомую нам функцию mutate<std::pair<Key, Value>>. Чего добру пропадать, мы ее в прошлый раз реализовали, так и валялась без дела. Дальше все очевидно: mutate возвращает тип Reverse<std::pair<Key, Value>>, где first и second поменялись местами. Мы сравниваем first и искомое значение типа Value. Зато не пришлось менять алгоритм поиска, только слегка обобщить его.
👍4🤔1
std::pony
Сегоня я нашел секретное предложение D2349R0, опубликованное в 2021 году за авторством некоего нечестивого Ville Voutilainen. На первый взгляд, этот коварный тип просто предлагает внести некоторые небольшие изменения в язык и крохотные улучшения в библиотеку. Однако более вдумчивое изучение мгновенно приведет вас в состояние когнитивного диссонанса:
1. Предложены новые спецификаторы my и little;
2. Предложено добавить в стандартную библиотеку новый класс pony;
3. Предложено добавить поддержку аллокаторов для нового класса.
Все эти, казалось бы, незначительные изменения незаметно подводят нас к появлению ПОПы (Пони-ориентированного программирования).
Например, теперь можно делать вот так:
my little pony<grin> twilight_sparkle;
twilight_sparkle.prance();
groom(twilight_sparkle);

Основной класс библиотеки представляет собой бесконечно расширяемый шаблон класса:
template <class T, class SpoilerAlert = void>
class pony {
friend class T;
};

Можно как угодно переопределять шаблон pony, если соблюдается главное и важнейшее требование:
- Нужно подружиться с первым шаблонным параметром, потому что Дружба - это волшебство!
Второй параметр шаблона нужен для поддержки аллокаторов:
1. Предлагается ввести новый аллокатор общего назначения, что-то вроде std::polymorphic_allocator, но лучше, поскольку он будет иметь более правильное имя std::ponymorphic_allocator.
2. Также предлагается разрешить псевдонимы в пространстве имен pmr, который связывает пониморфные аллокаторы с pony:
namespace pmr {
template <class T> using unicorn = std::pony<T, std::ponymorphic_allocator<T>>
}

Вместо старых и дурацких структур данных можно грациозно создавать целые конюшни поняток. Сравним код в старой парадигме с новым подходом.
Как мы писали бизнес-логику с применением разных хитроумных структур данных из стандартной библиотеки:
std::unordered_map<int, int> oink = foo();
static_assert(false, "This will never perform well enough, I'm quitting and going fishing");
std::terminate(); // just in case some idiot makes this compile one day; don't remove

И вот как теперь мы решаем все проблемы одним махом с помощью ПОПы:
std::pony<DivineOOPWrapper<int, int>> fabulous = foo();
make_lots_of_money_really_fast(fabulous);

По тексту видно, что автор переполнен гордостью за свое воистину дьявольское изобретение. Мол, если ребенок захочет пони, ему можно тут же вручить C++: там тоже есть std::pony. Не спешите радоваться! Во что же превратится наша секта снобов, если сюда прорвутся дети? Ладно, мало что изменится. А если заслуженный сеньор, сотню другую раз реализовавший статический вектор разными способами, начнет гуглить новый синтаксис этой блистательной парадигмы? Немало четких пацанов полегло на диване, загипнотизированных блистательным образом Пинки Пай. Немногие потом возвращаются из Эквестрии к нормальной жизни.
Да и вообще, как можно доверять коду человека, который знает наизусть слова песни "Милая пони"? Я надеюсь, у комитета хватит мужества и сил не поддаваться магии Искорки и отклонить это ужасно милое предложение.
😁7👏3
bimap. pervert edition
Жизнь - как коробка голубцов Ивана Семеновича, всегда знаешь, что внутри. Внутри горький вкус поражения. Поразительно, как много умников опередили тебя и реализовали очевидные идеи раньше. Раньше даже, чем ты родился. Иные разродятся такими извращенными методами, что все мозги вывихнешь в попытке преисполниться.
Взять хоть bimap, над которым я так долго ломал голову. Оказывается, Антон уже не единожды позаботился об этом. Я же пропустил запись доклада "C++ трюки из userver", посему находился в счастливом неведении и мучительно долго искал изящную реализацию контейнера. Наконец-то, ознакомившись с предложенным методом, отойдя от шока, испив киселя и окончательно успокоившись, я решился повторить пример.
Допустим, есть у нас небольшое перечисление:
enum EnumNE107Status {
EnumNE107Status_NE107_FAILURE,
EnumNE107Status_NE107_GOOD
};

Как же привязать к ним строчки, которые потом можно будет использовать в каком-нибудь самописце? Что если делать это через функцию? Звучит необычно, но вполне реализуемо.
constexpr TrivialBiMap kMyEnumDescription {[](auto selector) {
return selector
.Case(EnumNE107Status_NE107_FAILURE, "Failure")
.Case(EnumNE107Status_NE107_GOOD, "Good");
}};

Здесь мы использовали порождающую лямбду в качестве источника информации о значении перечисления и соответствующей ей строчке. Для некого объекта selector мы через метод Case последовательно перебираем все варианты, пока не найдем искомое.
Теперь, определив kMyEnumDescription, можно использовать метод TryFindBySecond, чтоб найти нужную голову по хвосту.
std::optional<EnumNE107Status> val {kMyEnumDescription.TryFindBySecond("Good")};
// val.value() == EnumNE107Status::EnumNE107Status_NE107_GOOD;

Либо воспользоваться методом TryFindByFirst, чтоб найти нужный хвост по голове.
auto name = kMyEnumDescription.TryFindByFirst(EnumNE107Status::EnumNE107Status_NE107_GOOD);
// name.value() == "Good";

В обоих случаях значение будет завернуто в std::optional, поскольку нельзя исключать коварство пользователя: он может подсунуть строчку или значение, которого нет в списке.
Сам псевдоконтейнер примерно так:
template <typename BuilderFunc>
class TrivialBiMap final {
public:
using TypesPair = std::invoke_result_t<const BuilderFunc&, SwitchTypesDetector>;
using First = typename TypesPair::first_type;
using Second = typename TypesPair::second_type;
constexpr TrivialBiMap(BuilderFunc func) : func_ {func} {}
constexpr std::optional<First> TryFindBySecond(Second const value) const {
return func_(EnumSwitch<First, Second>{value}).Extract();
}
constexpr std::optional<Second> TryFindByFirst(First const value) const {
return func_(EnumSwitch<Second, First>{value}).Extract();
}
private:
const BuilderFunc func_;
};

Типы First и Second даже не пришлось вводить вручную как параметры шаблона, потому как порождающая лямбда содержит достаточно информации о типах, и класс SwitchTypesDetector просто помогает вытащить это на поверхность. Предполагаю, вы уже поняли, как реализован EnumSwitch. Если не совсем, то a suivre.
👍4
bimap. endgame
Ходят слухи, что порождающую лямбду из предыдущего поста настойчиво разыскивает один фиолетовый гигант в перчатке. Это неудивительно, ведь эта функция - краеугольный камень футуристского подхода к задачам на однозначное соответствие. Наверняка это кто-то из функциональщиков додумался определять контейнер через цепь вызовов. Пропуская объекты различных типов через цепочку, можно решать разные задачи.
Как же в этом странном, но контейнере, найти нужное значение по ключу? Задача не слишком сложная, нужно лишь сварганить такой класс, вызов метода Case которого анализирует переданные аргументы и возвращает себя. В конструктор мы передадим искомое значение, сохраним его как константный член класса. Результаты изыскания лучше поместить в std::optional, ведь не факт, что переданный пользователем ключ вообще существует. Метод Extract без лишних телодвижений предъявит найденное.
template <class First, class Second>
struct EnumSwitch {
constexpr explicit EnumSwitch(Second const name)
: search_ {name}, result_{} {}
...
constexpr std::optional<First> Extract() {
return result_;
}
Second const search_;
std::optional<First> result_;
};

Осталось лишь определить метод Case, основной и центровой метод, ради которого все эти пляски с порождающими функциями и затевались. Чтоб не загромождать и без того длинный пост, предположим, что типы ключа и значения в нашем контейнере абсолютно разные. Вспомним, как мы используем Case внутри порождающей функции:
selector.Case(EnumNE107Status_NE107_FAILURE, "Failure")

Если мы используем прямой порядок агрументов, то поиск будет происходить по имени (Second const name == "Failure"):
constexpr EnumSwitch& Case(First const value, Second const name) {
if (!result_ && search_ == name) {
result_ = value;
}
return *this;
}

Если же мы поменяем аргументы местами, то поиск будет происходить по значению (Second const value == EnumNE107Status_NE107_FAILURE):
constexpr EnumSwitch& Case(Second const value, First const name) {
if (!result_ && search_ == value) {
result_ = name;
}
return *this;
}

Пару слов о таинственном SwitchTypesDetector, через которые мы получаем информацию о типах First и Second.
Он простой, как апельсин, содержит только метод Case, который возвращает объект шаблонного типа SwitchTypesDetected
struct SwitchTypesDetector final {
template <typename First, typename Second>
constexpr auto Case(First, Second) -> SwitchTypesDetected<First, Second> {
return SwitchTypesDetected<First, Second>{};
}
};

Вот таким нехитрым способом мы можем определить передаваемые в этот тип параметры шаблона, Case здесь возвращает сам себя.
template <typename First, typename Second>
struct SwitchTypesDetected final {
using first_type = First;
using second_type = Second;
constexpr SwitchTypesDetected& Case(First, Second) noexcept { return *this; }
};

Прелесть в том, что нам даже не приходится тратить время на вызов методов, все калькуляции происходят на этапе компиляции.
Осталось дело за малым - показать код сишникам и смотреть, как они медленно седеют.
👍7🤔1
A full office life
В коридоре, ведущему в покинутый опен-спейс, царила кладбищенская тишина. Когда-то здесь была жизнь, деловито сновали туда-сюда разработчики, как трудолюбивые муравьи. Других сюда бы и не взяли, жесткий отбор безжалостно отсеивал бездарей и лентяев! По крайней мере, они сами так говорили. Я им верю. Меня не взяли.
Теперь же вас здесь нет. А я тут, печально смотрю на ваши пустующие кубиклы, тесные загончики для рабочих лошадок.
Одно только название компании обладало поистине гипнотической силой. Кто же не захочет поработать международной корпорации? Это же так круто, даже если в итоге ты приобретешь лишь красивую строчку в резюме. Кандидаты с воодушевлением следовали сюда за HR, словно дети за Крысоловом.
Однажды чары развеялись, ведь компания без оглядки бежала из наших краев. Понуро разбрелись разработчики. Остался лишь огромный скелет компании, торчащие ребра кубиклов, где на светлых столах чернели остовы пустых системников, к которым сиротливо льнули мониторы, поджав выдернутые шнуры.
🤔7👍1🔥1
Спеша удалиться от геометрически упорядоченного и оттого тошнотворного места, я поднимался все выше и выше. Все сильнее ощущалось, что в здании уже давно никто и ничто не работает, даже кондиционеры. С каждым этажом все сильнее сгущалась духота. Миновав бедосирую столовую, приунывшую от того, что в ней уже никто не чавкает и не сербает горячий кофе, я попал в раскуроченную серверную. Давно остывшие стойки хранили гробовое молчание.
Зайдя в туалет покричать на белого друга, я обнаружил откидной пеленальный столик. Какой папаша-изверг притащит на работу грудного младенца? Разве что папаша Дювалье. В голове непроизвольно восстал образ крестьянки в царской еще России, которая берет с собой малыша на работу в поле. Прогресс идет по спирали.
На последнем этаже стало настолько жарко, что находиться в помещении стало невыносимо. Сломав хлипкий замок, я открыл необычайно большое окно.
"Уходить из офиса нужно эффектно!" — подумал я и поправил пневмокостюм.
👍4🤔1
Первомай вам не Mayday
Проснувшись в полдень, я точно знал, какое сегодня число. Первый день пятого месяца. Только вот что-то было не так, в этих цифрах крылся какой-то подвох. Нечто необычное витало в воздухе. Даже засомневался, нужно ли сегодня работать.
"Если сегодня выходной, то не стоит трудиться", - подумал я. - "Но как узнать, выходной сегодня или нет?"
Я задумался и открыл простенький редактор в терминале.
"Если сегодня суббота или воскресенье", - подумал я еще раз, - "то точно выходной".
Как же узнать, какой день недели сегодня? Задача не стоит и выеденного яйца, всяк имеющий дело со временем помнил последовательность преобразований.
Текущее время можно получить через time, потом преобразовать через localtime в структуру tm и посмотреть значение tm::tm_wday.
Тут я брезгливо поморщился. Черного кобеля не отмоешь до бела. Сколько ни облагораживай сишный код, а настоящего изящества и утонченности в нем не появится.
На этом пути важно вспомнить, что допустимые значения tm_wday - целые от 0 до 6, и что у буржуев неделя начинается с воскресенья. У них там всех фляга свистит погромче, чем на балабановской спичечной фабрике.
Использование пространства имен std только внешне делает код похожим на c++:
  std::time_t t = std::time(0);
std::tm* now = std::localtime(&t);
std::cout << now->tm_wday << std::endl;

Сишной сути это не меняет.
Еще одно умственное усилие, и всплывает воспоминание о том, что вроде бы в c++20 появились календари, которые не врут.
День в месяце можно задавать по-разному: через порядковый номер, как мы обычно и делаем, либо задать его днем недели с номером. Такое тоже встречается в жизни, например, санитарный день в библиотеке может проходить каждый третий четверг месяца. Естественно, мы можем преобразовать одно представление в другое.
Нужное нам представление даты реализовано в классе std::chrono::year_month_weekday. В конструктор которого можно передать некий объект класса std::chrono::sys_days, а это не что иное как std::chrono::time_point<std::chrono::system_clock, std::chrono::days>. То есть, грубо говоря, системное время с точностью до дня. Метод std::chrono::system_clock::now() возвращает системное время с точностью до наносекунды, что избыточно в нашем случае. Хорошо, что можно подкрутить точность функцией std::chrono::floor.
Вуаля! Проверка дня недели:
 auto now = std::chrono::system_clock::now();
std::chrono::year_month_weekday const ymd{std::chrono::floor<std::chrono::days>(now)};
if (ymd.weekday() != std::chrono::Sunday && ymd.weekday() != std::chrono::Saturday) {
std::cout << "working day: " << ymd.weekday() << std::endl;
}

Метод ymd.weekday() возвращает объект класса std::chrono::weekday, который имеет собственный formatter для вывода, так что на экране появится название дня недели, а не просто бездушный номер.
После запуска в терминале появилась строчка working day: Wed.
Ничего не поделаешь, бесстрастный кремниевый судья сказал, что надо трудиться, и я со вздохом сел писать пост...
P.S. С праздником!🥳
👍9
С Днем радио!🥳
Хороший день, чтоб продолжить писать кое-что для SDR, например. Немного грустно писать на чистом Си, но таков путь. Вроде бы всегда ровно дышал к умным указателям, а сегодня уже тоскуешь по ним. Подышав на стекло, медленно и печально выводишь пальцем "shared_ptr". Кстати, в том докладе Антона, где он рассказывал про Bimap (C++ трюки в userver), есть интересная загадка про std::shared_ptr. Сформулирована она так:
return std::shared_ptr<Logger>(
std::shared_ptr<void>{},
&GetNullLogger()
);

А теперь, уважаемые знатоки, через минуту скажите, что все это значит?
Если приглядеться, то это явно какой-то конструктор shared_ptr, экзотический и малополезный, коих у этого умного указателя предостаточно.
cppreference определяет его как aliasing constructor, конструктор псевдонимов. Такого определения я не нашел в стандарте, зато он четко и ясно, будто суровые парни на районе, поясняет за такой конструктор.
template<class Y> shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;
template<class Y> shared_ptr(shared_ptr<Y>&& r, element_type* p) noexcept;

Эффект: Создает shared_ptr который сохраняет p и разделяет владение оригинальным объектом с указателем r.
Постусловие: get() == p. Для второй перегрузки указатель r будет пуст, r.get() == nullptr. (Появилось в c++20, со второй перегрузкой надо быть настороже)
Примечание 1. Этот конструктор доведет вас до провисания указателя, пока группа владения r не рассосется.
Примечание 2. Этот конструктор позволяет создать пустой shared_ptr с ненулевым указателем на борту.
Примечания как раз и намекают на правильный ответ на нашу загадку. Покажем, как предполагалось использовать этот конструктор. Допустим, есть у нас структура A с очень важной информацией внутри:
struct A { int x; };

Тут же появится и структура B с очень важной структурой A внутри:
struct B {
A a;
int y;
};

Допустим, мы хотим совместно владеть объектом типа B и используем shared_ptr.
std::shared_ptr<B> b {std::make_shared<B>()};

Но вдруг где-то нужно использовать указатель на A и не абы какой, а именно на объект внутри общего B. Тогда мы можем использовать описанный выше конструктор:
std::shared_ptr<A> a {b, std::addressof(b->a)};

Если указатель a достаточно умен, чтоб остаться в живых последним, то можно не беспокоиться за указатель на структуру A, он точно будет валидным даже после уничтожения b.
Созданный внутри b объект типа B еще живой, его держит указатель a.
Вот только проблема в том, что второй параметр конструктора вовсе не обязан быть хоть как-то связанным с оригинальным объектом. Передать можно любой адрес.
Никто не запретит мне хранить таким образом некий тип C:
struct C {};
std::shared_ptr<C> c {b, new C};

Это работает, но при уничтожении указателя c никто и не подумает удалить созданный объекта типа C. Однако даже такой указатель будет держать объект из b в заложниках.
Чтоб избежать порочной связи, можно в качестве базового указателя скормить нулевой shared_ptr, который внезапно не владеет ничем, как погоревший мздоимец.
std::shared_ptr<C> c {std::shared_ptr<С>{}, new C};

И тип этого "ничего" не так и важен: это может быть A, B или C, а может и int, даже void. Собственно, его мы и видим в оригинальной загадке. Итак, правильный ответ: возвращается банальный сырой указатель в шкуре shared_ptr!
Который раз отправляю эту шараду в 13й сектор одной известной телевизионной игры. Уж больно охота посмотреть, как от удивления господин Ведущий упадет из-под купола охотничьего домика, или откуда он там вещает загробным голосом? Но пока не везет.
👍4🤯21
std::out_ptr_t
Однажды на каком-нибудь интервью Главный Разработчик в конторе Horns-n-Hooves Ltd. спросит вас: "А какие умные указатели вы знаете?". Не упустите момент и смело вывалите ему на очки не только скучный unique_ptr и слабосильный shared_ptr, но и сопутствующие шаблонные классы-адаптеры. Если и есть что-то хорошее в нововведениях с++23, то это тип std::out_ptr_t.
Он был предложен жарким летом 2018 года в документе P1132R0 некими JeanHeyd Meneide, Todor Buyukliev и Isabella Muerte. Авторы сего опуса неплохо начали, написав, что out_ptr_t - это вроде как Моисей, который приведет сишный API и умные указатели в землю обетованную. Не слишком научно, но мне как-то спокойнее, если предложения для плюсов пишут религиозные люди. Ибо они, убоявшись геенны огненной, не станут предлагать использовать венгерскую нотацию или какой-нибудь garbage collector.
В общем, этот вспомогательный класс пригодится тем, кому просто необходимо использовать сишные библиотеки в работе. Очень часто там приходится начинать с вызова функции, которая создает некую точку входа, условный дескриптор для управляемого объекта. Могу привести в пример библиотеку hackrf для SDR.
Заурядное начало работы выглядит так:
hackrf_init();
hackrf_device *device = NULL;
hackrf_open(&device);

Последняя функция выдает нам правильный указатель на управляемый объект.
Вот затрапезная концовка работы:
hackrf_close(device);
hackrf_exit();

Чистый и незамутненный си. Какая гадость! Нужно срочно переписать это на божественном с++, да так, чтоб было сплошное RAII.
Никто не запрещает использовать красивый и умный указатель для управления ресурсами уже сейчас.
auto delete_hackrf = [](hackrf_device* device){ hackrf_close(device); };
std::unique_ptr<hackrf_device, decltype(delete_hackrf)> dev {};
hackrf_device *device {};
hackrf_open(&device);
dev.reset(device);

Выглядит это, откровенно говоря, не очень привлекательно. Слишком много танцев вокруг функции инициализации приходится исполнять, так можно и ногу потерять в процессе.
Как же заставить сишный API работать с умным указателем более изящно? Здесь на помощь к нам бежит специальный класс-адаптер out_ptr_t.
std::unique_ptr<hackrf_device, decltype(delete_hackrf)> dev {};
hackrf_open(std::out_ptr(dev));

или то же самое для shared_ptr:
std::shared_ptr<hackrf_device> sh_dev {};
hackrf_open(std::out_ptr(sh_dev, delete_hackrf));

Сколько грации в этой паре строчек! Класс берет на себя правильную инициализацию умного указателя внутри сишной функции, а функция std::out_ptr берет на себя создание правильного объекта std::out_ptr_t, т.е. выведение аргументов шаблона. Ведь полное описание шаблона выглядит как:
template <class Smart, class Pointer, class... Args> class out_ptr_t;

Вручную такого монстра уж точно не захочется создавать. Впрочем, как раз это и не рекомендуют делать, и я догадываюсь почему...
👍3🤔2😁1
inside of std::out_ptr_t
Теперь, когда широкой общественности стало известно о существовании out_ptr_t, некоторые разработчики не могут спокойно пить смузи и есть фалафель, измазанный в хумусе. Спать им и так некогда, надо работать! Но прежде всего, надо хотя бы одним глазком заглянуть в нутро out_ptr_t.
Стандарт в разделе 20.3.4.1 Class template out_ptr_t [out.ptr.t] дает приблизительное описание этого класса:
template<class Smart, class Pointer, class... Args>
class out_ptr_t {
public:
explicit out_ptr_t(Smart&, Args...);
out_ptr_t(const out_ptr_t&) = delete;
~out_ptr_t();
operator Pointer*() const noexcept;
operator void**() const noexcept;
private:
Smart& s; // exposition only
tuple<Args...> a; // exposition only
Pointer p; // exposition only
};

В конструктор объекта такого типа передается ссылка на указатель, возможно, даже не самый глупый, который будет храниться в s.
Далее угадывается настолько классическая схема работы с хранением параметров, что хочется воскликнуть: "Граждане! Храните отложенные аргументы в tuple. Если, конечно, они у вас есть". Последний член класса p получает почетный nullptr в качестве начального значения. Допустим, Smart - это все же тип умного указателя (хотя out_ptr_t работает и с другими типами), тогда у него точно есть метод reset(), и он будет вызван немедленно.
У класса есть два метода приведения типов, которые могут быть использованы для доставки указателя внутрь out_ptr_t.
std::out_ptr_t<Smart,Pointer,Args...>::operator Pointer*
std::out_ptr_t<Smart,Pointer,Args...>::operator void**

Делают они примерно одно и то же, выдают адрес указателя p: addressof(const_cast<Pointer&>(p)).
В деструкторе мы наконец-то помещаем значение указателя в некую умную субстанцию s тем или иным способом, например:
if (p) {
apply([&](auto&&... args) { s.reset(p, std::forward<Args>(args)...); }, std::move(a));
}

apply нам нужен только для того, чтоб распотрошить наш tuple c сохраненными аргументами и передать их методу reset как следует.
Теперь давайте проверим, действительно ли создавать std::out_ptr_t - это плохая идея?
Создадим специально для наших стыдных опытов не очень умный указатель:
template <class T>
struct stupid_ptr {
void reset(T *t = nullptr) { ptr = t; }
T* ptr = nullptr;
};

std::out_ptr_t очень терпим и не откажется работать даже с таким убогим классом.
stupid_ptr<hackrf_device> p {};
std::out_ptr_t<std::stupid_ptr<hackrf_device>, hackrf_device*> g {p};
hackrf_open(g); // p.ptr == nullptr
g.~out_ptr_t<std::stupid_ptr<hackrf_device>, hackrf_device*>(); // p.ptr != nullptr

Работает ожидаемо, как и описано на cppreference. Указатель, что выдает нам hackrf_open, хранится в некой временной переменной, и только в деструкторе телепортируется в масло.
Однако, если мы обратимся к стандартным типам, то и поведение, и даже размер объекта изменится.
std::unique_ptr<hackrf_device, decltype(close_hackrf)> p {};
std::out_ptr_t<std::unique_ptr<hackrf_device, decltype(close_hackrf)>, hackrf_device*> g {p};
hackrf_open(g); // p.get() != nullptr
g.~out_ptr_t<std::unique_ptr<hackrf_device, decltype(close_hackrf)>, hackrf_device*>();

Как оказалось, здесь std::out_ptr_t не ждет вызова деструктора, а сразу помещает указатель в unique_ptr. Если сравнить размер out_ptr_t<unique_ptr> с out_ptr_t<stupid_ptr>, то первый будет в два раза меньше. Из этого можно сделать вывод, что вспомогательный член класса Pointer p отсутствует. Если же мы заглянем в исходники, то найдем подтверждение нашей догадке и ужаснемся: этот тип вовсе не так прост, как кажется. У него богатый внутренний мир, полный частичных специализаций для разных типов указателей.
Вывод уже напрашивается сам собой: тарелку после гречки лучше мыть сразу, ночью надо спать, а функцию std::out_ptr - обязательно использовать.
👍4🤔1
std::inout_ptr_t
Наконец-то жуткая холодная весна заканчивается, и лето уже бесстыдно заглядывает нам в окна. Нарциссы совсем распустились. И цветы тоже не отстают.
Вот иду я по измокнувшей в солнце улице, и гложет меня нехорошее чувство. Забыл совсем рассказать, что в заголовочном файле <memory> новых классов появилось несколько. Про std::out_ptr_t мы уже все знаем, но в лучших традициях индийского кино, у этого адаптера есть еще и младший брат: std::inout_ptr_t.
Ясное дело, отличие не только в названии, хотя они и похожи, как ухо и второе ухо. Если посмотреть внимательней, станет понятно, что этот бедный родственник живет за счет out_ptr_t.
template<typename _Smart, typename _Pointer, typename... _Args>
class inout_ptr_t {
using _Out_ptr_t = out_ptr_t<_Smart, _Pointer, _Args...>;
using _Impl_t = typename _Out_ptr_t::_Impl_t;
_Impl_t _M_impl;

Зачем же понадобился нам этот сородич?
Мы знаем, что out_ptr_t используется исключительно для добычи указателя из функции. Не об этом ли говорит имя типа? В doxygen-комментариях есть что-то похожее, в описании аргумента функции можно было добавить "направление параметра": @param[in], @param[out] и @param[in,out].
С направлением out все понятно, указатель передается в функцию, чтоб туда значение было записано.
/**
* @param[out] p_param
*/
void makeSense(int * p_param) {
*p_param = 42;
}

Направление in говорит о том, что значение читается и как-то используется в функции. Комбинация in,out намекает, что значение по указателю не только используется, но и на его место записывается некий результат.
Так же и inout_ptr_t дает нам возможность использовать первоначальное значение указателя, он не сбрасывается как в случае out_ptr_t.
Возьмем пример прямо из стандарта, благо имена там красивые. Допустим, есть некоторая функция, которая возвращает указатель на выделенную память под объект типа star_fish.
struct star_fish* star_fish_alloc();

И есть другая функция, которая должна освободить память по переданному указателю и записать туда новое значение на свежевыделенный участок.
int star_fish_populate(struct star_fish** ps, const char* description);

Небольшая структура обеспечивает окончательное освобождение многострадальной памяти.
struct star_fish_deleter {
void operator() (struct star_fish* c) const noexcept;
};

std::unique_ptr<star_fish, star_fish_deleter> peach(star_fish_alloc());
// здесь мы используем объект, а потом пересоздаем его
star_fish_populate(std::inout_ptr(peach), "caring clown-fish liker");

Если развернуть необычные конструкции, то мы выполняем следующие шаги:
int* peach_raw = peach.release();
star_fish_populate(&peach_raw, "caring clown-fish liker");
peach.reset(peach_raw);

При инициализации inout_ptr_t вызывается функция release(). При этом текущее значение указателя сохраняется. Инициализация в исходниках выглядит как _M_ptr = _M_smart.release();
Затем это же значение используется в функциях преобразования operator Pointer*() const и operator void**() const. Таким образом, внутри функции star_fish_populate можно получить старое значение указателя, чтоб освободить его.
И уже традиционно, не рекомендуется использовать inout_ptr_t в чистом виде, только через функцию std::inout_ptr. "Оно вкусно и на цвет красиво!"
👍31🤔1
std::ssize
С тех пор как проект закрылся, я живу под мостом и единственный вопрос, который меня ныне интересует: как определить размер контейнера? Можно использовать рулетку либо длинную палку. Если дело совсем плохо, то сделать замеры можно и ногой. Однако лучше вспомнить про функцию std::size, которая сама определит, что за контейнер пытаются ей скормить: продвинутый плюсовой со встроенным методом size(), либо рабоче-крестьянский сишный массив. Демиурги стандартной библиотеки добавили эту функцию в стандарте с++17, дабы умилостивить бога обобщенного программирования и облегчить работу ленивым разработчикам. Реализация для контейнеров предельно простая:
template <typename Container>
constexpr auto size(Container const &cont) -> decltype(cont.size())
{ return cont.size(); }

Для массивов есть своя специализация функции:
template <typename Tp, size_t Nm>
constexpr size_t size(const Tp (&)[Nm])
{ return Nm; }

Четко, понятно, доходчиво. Казалось бы, что тут можно улучшить? Однако в с++20 появляется новая функция std::ssize. Зачем понадобился еще один метод, делающий все то же самое?
Олдфагам может показаться имя ssize ностальгически знакомым. Не показалось, действительно, есть такой тип ssize_t, назначением которого было возвращать размер чего-нибудь либо отрицательное значение в качестве кода ошибки. Этакий optional на минималках. Собственно, и для функции std::ssize вся разница заключается в типе возвращаемого значения. Разница незначительная на первый взгляд, но тут кроется фундаментальный сдвиг в восприятии размеров и индексов.
Стандартная библиотека смело заявила, что отрицательные индексы и размеры применительно к контейнерам не имеют смысла. Например, есть у нас некоторый std::array<X, 10U>, размер его всегда строго положительная величина: constexpr size_type size() const, как и индексы в операторе reference operator[](size_type __n).
Изначально же отрицательный индекс массива не был лишен смысла, например, если есть указатель на середину массива ptr, то ptr[-1] значит лишь смещение относительно текущего положения *(ptr - 1).
Тут назревает конфликт стандартной библиотеки и адресной арифметики. Чаще всего размерные типы беззнаковые, а типам индексов хотелось бы быть знаковыми. Почти всегда размер используется для каких-нибудь махинаций с индексами. Тогда такое смешение знаковых и беззнаковых типов - бескрайнее поле для посева ошибок.
В предложении p1227R1 описывается такой пример:
template <typename T>
bool has_repeated_values(const T& container) {
for (int i = 0; i < container.size() - 1; ++i) {
if (container[i] == container[i + 1]) return true;
}
return false;
}

Отстреливший не одну ногу разработчик сразу найдет выражение container.size()-1 опасным. Поскольку результат size() беззнаковый, то при нулевом размере size()-1 легким движением руки превращается в огромное положительное число. Последствия могут быть воистину катастрофическими. Это как если бы Ромео был из семьи uint32_t , а Джульетта - из int32_t. В результате все умерли, чума на оба ваших дома! Зато ssize вернет результат size(), приведенный к знаковому типу или чему-то вроде std::ptrdiff_t, поэтому ничего страшного не случится.
👍6👏1
std::auriga
От неожиданных звонков никогда не жди ничего хорошего. Хуже только звонки с незнакомых номеров, которые я уже давно игнорирую. Цукербрин так и не позвонил, а посвящать разного рода мошенников в тонкости работы афедронного коллайдера или обсуждать явные признаки каллигатической остеополлюции всех их родственников до седьмого колена терпенья уже не хватало. Однако сбросить этот звонок я не смог: весело тренькал тимс, приглашая присоединиться к внезапному рабочему собранию.
Вызов принят.
- Парни, мне очень жаль... - начал наш менеджер со стороны заказчика.
Тут сердце мое упало. Когда менеджер заводит разговор с командой таким образом, это тревожный звоночек. Тем не менее, скорбные лица коллег ухитрялись выражать облегчение. Между собой мы уже пару лет мрачно шутили на тему неизбежного увольнения. Геополитика.
- ... я ничего не мог поделать, все было решено там, - менеджер ткнул пальцем куда-то вверх, а по его щеке неожиданно покатилась горькая скупая слеза. Нам было его жаль, мировой мужик, теперь обречен вытягивать неподъемный проект в окружении сомнительных разработчиков из восточной Европы. На его месте мы бы тоже зарыдали.
Видел его таким же несчастным лишь однажды, когда, будучи в командировке, мы пошли в столовую, и я налил себе пару ложек супа в огромную миску. Кто же знал, что у них там порция оценивается по объему тары? Ему, на правах хозяина, пришлось изрядно раскошелиться.
Впрочем, никакие сентиментальные сцены не смогли смягчить удар об стену оборудования заказчика после окончательной блокировки моего рабочего аккаунта. Я отдал проекту пять лет, за которые успел поседеть от созерцания их кода, а потом какой-то Ганс решает, что все и без нас заработает... Удачи, попутного ветра в горбатую спину.
Я же отправился навстречу черной меланхолии.
Знаю, многих интересовал вопрос, где Аурига, куда пропала? Слухи ходили разной степени упорности.
Только сплетни о смерти были сильно преувеличены. Компания и сама бы могла развеять туман неопределенности, однако, судя по изменениям на официальном сайте, она была всецело охвачена модным ныне явлением ребрендинга. Сами можете оценить результаты, лично я бы для колорита добавил больше матрешек с балалайками верхом на медведях. Четко обозначить намеченный вектор развития.
Те сотрудники, кто посещает офис ради неповторимой рабочей атмосферы, сочных фруктов или массажа языка возле кулера, свидетельствуют, что компания дышит, народ в столовую ходит, эйчары арканами перспективных кандидатов ловят. Вот для меня Аурига чуть было не закончилась, так сильны были негодование и досада. Перспектива снова работать на проклятых буржуинов меня нисколько не прельщала. Подумывал даже заняться написанием детских стихов, однако мои вирши про истекающую соком Луну не приняли ни в одном издательстве, заинтересовались только в местном лечебном заведении закрытого типа, куда рукопись попала по ошибке. К счастью, не пришлось даже кодить в переходе за донаты, ведь внезапно очень многим отечественным компаниям просто как воздух стали необходимы грамотные специалисты. Те, кто спер у компаний с мировым уровнем не только разбитый ноутбук, но и бесценный опыт разработки качественных продуктов.
😁6👍5
std::memcpy_s
Как-то в одной экзотической для меня IDE решил скопировать пару массивов, недолго думая, реализовал через memcpy. Вдруг среда разработки заволновалась и как выдаст предупреждение:
Call to function 'memcpy' is insecure as it does not provide security checks introduced in the C11 standard. Replace with analogous functions that support length arguments or provides boundary checks such as 'memcpy_s' in case of C11

Забавно, все беды от небезопасного копирования в memcpy. Мы и не знали.
Оказывается, еще в 2003 году некто Martyn Lovell изложил некоторые крамольные мысли в докладе WG14/N997 Proposal for Technical Report on C Standard Library Security.
Автор стонал, что стандартные функции появились в незапамятные времена, когда для безопасного программирования достаточно было каски, чтоб выпавший пудовый трансформатор не сбил с мысли. Автор жаловался, что использовать сейчас такой грубый интерфейс основных функций некуртуазно и опасно. Изнеженный домашний программист может легко пораниться. Автор канючит, что надо постепенно и осторожно принять альтернативные версии каждой из опасных функций и объявить оные устаревшими. Разработчик же подобен лягушке: если варить медленно, то далеко не упрыгает.
Впрочем, даже месье Мартин понимал, что простое переключение на эти новые функции само по себе не обеспечит безопасность. Только массовые расстрелы, анализ кода и тщательное тестирование спасет разрабатываемое приложение. Он просто надеется, что новые функции снизят количество тривиальных ошибок кодирования в области безопасности.
В современных реализациях стандартной библиотеки C есть три типа проблем. Две из которых нам совершенно неинтересны. Сосредоточимся на первой, где функции страдают оттого, что им не передают параметры для безопасной реализации. Сюда якобы и попадает наша любимая функция memcpy.
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

Думаю, все знают, что функция делает, однако стандарт не говорит ничего о том, что такое n. Это не размер s1 или s2, это просто количество байт, которые будут скопированы из s2 в s1.
Наверное, это душераздирающе опасно. Так и хочется решить эту проблему, добавив новую функцию с соответствующими параметрами в стандарт C. А чтоб хорошо запоминалось, к стандартному имени добавим окончание _s. Хотя лучше бы добавить _ms. Чтоб сразу было понятно, что ноги у этого документа растут из компании Микрософт. Что породило волну шуток про квалификацию мелкомягких работников, которые не в состоянии проверить размер буфера.
Тем не менее, все эти функции со странными окончаниями таки протащили в стандарт. Теперь в разделе Annex K гордо перечислены все новые функции из bounds-checking interfaces.
Правда, оговорка имеется. В стандарте сказано, что только реализации, которые определяют дефайн __STDC_LIB_EXT1__ должны соответствовать этому дополнению. Если не определяет, то и не обязаны все эти сомнительные функции предоставлять.
Также, чтоб не загрязнять пространство имен, пользователь должен сам объявить __STDC_WANT_LIB_EXT1__ до подключения заголовочных файлов. Правда, некоторые компиляторы плевать хотели на это правило, да, MinGW?
Тем не менее, в некоторых средах можно добраться до стильной, модной функции
errno_t memcpy_s(void * restrict s1, rsize_t s1max, const void * restrict s2, rsize_t n);

Какие проверки берет на себя функция:
- s1 и s2 не должны быть нулевыми указателями, иначе обидится и вернет EINVAL.
- s1max и n не должны превышать RSIZE_MAX.
- n не должно быть больше s1max, иначе вернет ERANGE.
Если проверка провалилась, как асфальт на улице Варварской, то функция запишет s1max нулей в s1, но только если s1 не нулевой указатель и s1max не больше RSIZE_MAX.
Если нарушения нет, то функция вернет ноль.
Даже если не учитывать тот факт, что это все - порождение корпорации зла, все равно полезность новых функций в стандарте вызовет споры с элементами поножовщины и в небольшом дружном сообществе. Собственно, реализацию нового интерфейса можно обнаружить только в компиляторах для Windows. Почему? A suivre.
👍4🤔1
is there std::memcpy_s?
Если вы всегда хотели намекнуть коллегам, что их пулл-реквест не очень, но не могли подобрать слов, то просто почитайте обзор экспертов на bounds-checking interface (пресловутое Annex K). Эти изменения никого не оставляли равнодушными. Кто-то к ним отнесся хорошо, как авторы документа N1967 Carlos O'Donell и Martin Sebor. Этот объемный документ написан в 2015 году. Примечательно, что в разделе "Доступные реализации" написано:
Хоть спецификация безопасного интерфейса существует уже более 10 лет, ее реализаций разной степени полноты не так уж и много. Есть две реализации в проектах с открытым исходным кодом, но ни в одном из популярных дистрибутивов, таких как BSD или linux, они не стали доступными для пользователей. А GNU C Library вообще неоднократно отклоняла предложения о включении по причинам, указанным Austin Group в их первоначальном обзоре TR 24731-1 N1106. Маловероятно, что API-интерфейсы будут включены в будущие версии этих дистрибутивов.

Забавно, но за четыре года ничего не поменялось, Robert C. Seacord в 2019 году напишет почти слово в слово такой же вывод.
Да уж, прочитав отчет этой остинской группы, сомневаешься, что это вообще когда-либо подпустят к обязательной части стандарта. Какие же правильные слова нашли парни из Austin Group, чтоб очернить предложенные Микрософтом изменения? Это настоящая методичка по языку ненависти!
Во-первых, группа приветствует столь пристальное внимание к проблемам переполнения буфера.
Во-вторых, группа предполагает, что такой подход, передача длины в качестве аргумента в некоторых строковых функциях, может привнести столько же проблем, сколько и решить. Ведь нет никаких гарантий, что программист поставит в качестве аргумента какое-то разумное значение. В итоге это все приведет к замутнению использования изначально четкой и понятной функции. Даже функции, выделяющие память с помощью malloc(), обеспечивают более безопасные, понятные и надежные интерфейсы.
В-третьих, загрязняется пространство имен.
В общем, многие находят предложенный TR спорным, и остинская группа не выказывает ему решительной поддержки. Приведу в пример наиболее выдающихся мыслителей Остина:
Curtis Smith не думает, что этот TR будет особенно полезным и сомневается во всеобщем одобрении предложения из-за неудобных имен с окончанием _s. Тем более, все, кто хотел проверять границы массивов, уже давно написал свои функции. Еще он рекомендует желающим жонглировать строками выучить и использовать с++, конечно, если их не сильно беспокоит производительность. Хотя, если кого-то беспокоит производительность, тот не станет вызывать функции с избыточными проверками.
Paul Eggert сразу предлагает голосовать против. Предложение спорное и может привести к забаговыванию ПО, да еще и не отражает консенсуса в сообществе.
Еще он вспоминает, как маэстро Торвальдс писал о похожем предложении:
Этот код медленный, уродливый, непонятный, непереносимый и не более безопасный по сравнению с оригинальным.
Вкратце, это просто тупой код.
Но если вы хотите на виду у всех впрягаться за тупой код, воля ваша. Только, пожалуйста, не надо этим кичиться.

Собственно, мейнтейнеры GNU C Library glibc отвергли похожее предложение по тем же причинам.
Ulrich Drepper, например, писал:
Это ужасно неэффективная чуханина от BSD. Учитывая историю противоречий, я удивлен, обнаружив это предложение опять на рассмотрении.
Неудивительно, что после такой критики "безопасные" функции и близко не подпустили к c++.
Как вы знаете, в плюсах очень рекомендуют включать плюсофицированные заголовочные файлы: не string.h, а cstring. Тогда можно использовать натурализованный memcpy из пространства имен std. Так вот, никаких страшных функций с окончанием _s в том пространстве нет и не будет никогда.
👍4😁1
memcpy_s implementation
Вот сижу, смотрю, как солнце красиво, будто сияющий клифф-дайвер, падает в воду. Стремительно, как и шансы Annex K попасть в настоящий стандарт. Вы уже поняли, что никто не собирается реализовывать микрософтовские супер-пупер функции, но есть штрейкбрехеры, которые таки поспешили с реализацией странного. Давайте посмотрим на их поделия.
Наши китайские друзья вот так представляют себе тело функции:
errno_t __cdecl memcpy_s(void * dst,
size_t sizeInBytes,
const void * src,
size_t count) {
if (count == 0) {
/* nothing to do */
return 0;
}

/* validation section */
_VALIDATE_RETURN_ERRCODE(dst != NULL, EINVAL);
if (src == NULL || sizeInBytes < count) {
/* zeroes the destination buffer */
memset(dst, 0, sizeInBytes);
_VALIDATE_RETURN_ERRCODE(src != NULL, EINVAL);
_VALIDATE_RETURN_ERRCODE(sizeInBytes >= count, ERANGE);
/* useless, but prefast is confused */
return EINVAL;
}
memcpy(dst, src, count);
return 0;
}

Впрочем, они божатся, что все основано на исходном коде crt микрософта, который он когда-то предоставлял. Выглядит правдоподобно, я верю. Внутри функции memcpy_s мы сразу находим вызов memcpy. Похоже, новые функции никогда не смогут побить рекорд скорости старичков.
Перед одлскульной функцией копирования стоят всего-навсего многословные проверки на отсутствие тривиальных ошибок, или, как еще их называют, sanity check.
Макрос _VALIDATE_RETURN_ERRCODE примерно описывается как:
#define _VALIDATE_RETURN_ERRCODE( expr, errorcode ) {  \
int _Expr_val=!!(expr); \
if ( !( _Expr_val ) ) { \
errno = errorcode; \
return ( errorcode ); \
}} \

Смысл действия очевиден, если выражение expr не true, то записываем код ошибки в errno и экстренно выходим и возвращаем тот же номер ошибки.
Проверяется, например, указатель на источник и приемник, которые не должны быть нулевыми. В такой проверке, конечно, ничего дурного нет, но иногда это вовсе излишне. Например, если это локальные переменные:
int src;
uint8_t dst[4];
...
memcpy_s(dst, 4, &src, sizeof(src)); // перебор

Никогда в здравом уме вы бы не стали их проверять, тратить на это драгоценные такты процессора!
Вообще, sanity check - дело тонкое, должен исходить из ситуации и здравого смысла, а если у вас есть какой-то универсальный тестер для любых типов данных, то, скорее всего, работает он не очень эффективно. Хорошо, что есть c++, где некоторые проверки можно и вовсе опустить за счет возможностей языка. Например, использовать ссылку вместо указателя. Банально? Однако шанс получить разыменование нулевого указателя внутри функции стремится к нулю.
👍3😁1