C++ Embedded
424 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
unforgivable idioms
Из коридора послышалась знакомая тяжелая поступь. Студенты притихли. Клацающие шаги преподавателя звучали все громче, и вот дверь распахнулась от удара деревянной ноги. Людская молва прочно связывала отсутствие конечности со слишком уж глубоким погружением в темное искусство плюсов. Наконец в аудиторию ввалился и сам легендарный и пугающий Федя "Красный глаз".
Не тратя слов на приветствия, он сразу взял с места в карьер:
- Проректор по воспитательной работе считает, что вы еще недостаточно взрослые, чтоб рассказывать вам об ужасах кровавого энтерпрайза. Чушь! Лучше подготовить вас сейчас. Чтоб потом вы не остолбенели от страха, внезапно увидев нечто жуткое в продакшене! Ведь какой-нибудь питонист не станет посвящать вас в свои планы, собирать митинг, а просто воткнет вам UB в спину...
Федя поморщился, явно вспомнив что-то мерзкое, а глаз угрожающе налился кровью.
ПОСТОЯННАЯ БДИТЕЛЬНОСТЬ! Если AUTOSAR не дает играть с препроцессором, то это неспроста! Правило A16-0-1 запрещает использовать препроцессор для чего бы то ни было кроме управляемого подключения заголовочных файлов. Иначе рано или поздно ваш класс станет жертвой Public Morozoff!
Эта древняя идиома абсолютного подчинения заставляет класс добровольно раскрыть все свои приватные члены.
#define private public
#define protected public

Пропиши их перед нужным хидером и делай с классом что хочешь! Хотя если класс выглядит вот так:
class Secret {
int32_t id {0x12345678};
int32_t code {42};
};

то придется усилить нажим:
#define class struct

С последним макросом надо быть осторожней, если когда-то это и проходило безболезненно, то сейчас не факт. Работа шаблонов может быть нарушена, например template<class T> после такого фокуса перестанет собираться.
Вы молодые, шутливые, вам все легко. Это не то. Это не просто уродливо смотрится, а еще и незаконно. Ибо стандарт в разделе 17.6.4.3.1 Macro names [macro.names], стихе втором четко говорит:
Единица трансляции не должна содержать #define или #undef имен идентичных ключевым словам языка.

Однако это не единственная идиома, позволяющая добраться до ваших потаенных членов класса. Поэтому ПОСТОЯННАЯ БЛИТЕЛЬНОСТЬ!
Пытка приведениями - второй непростительный прием, прямо запрещенный АUTOSAR-ом в правиле A5-2-4: reinterpret_cast не должен применяться никогда.
Следующий код не просто причиняет боль,
Secret s {};
uint8_t *tmp = reinterpret_cast<uint8_t *>(&s);
int *code = reinterpret_cast<int32_t *>(tmp + sizeof(int32_t));
std::cout << "secret: " << *code << std::endl;

но и проворачивает объект s в байтовый фарш, а затем, зная смешение элемента code, получает доступ к полю.
Можно действовать более изящно, использовать указатель на член класса int32_t Secret::*
using member = int32_t Secret::*;
Secret s {};
uint64_t offset{4};
member m {std::bit_cast<member>(offset)};
std::cout << "secret: " << s.*m << std::endl;

Здесь нужно лишь правильно подобрать смещение и преобразовать его указатель. Смещение, например, скорее всего, измеряется в байтах от начала занимаемой объектом памяти. Забавно, но даже reinterpret_cast откажется выполнить такое преобразование, а вот bit_cast не побрезгует, у него вообще нет сострадания. Кстати, объект member, созданный по умолчанию, вовсе не содержит в себе нулевое смешение, там внутри будет -1, поэтому получить доступ к первому объекту id не получится. Класс хранит свои секретики.
Если и этот метод недостаточно болезненный, то можно переложить вычисление смещения на компилятор. Нужно лишь воссоздать теневой публичный двойник класса (хотя идентичность, понятное дело, не гарантирована).
struct SecretPub {
int32_t id;
int32_t code;
};

Тогда указатель на член класса можно получить проще.
using member = int32_t Secret::*;
Secret s {};
member m {reinterpret_cast<member>(&SecretPub::code)};
std::cout << "secret: " << s.*m << std::endl;

Разновидностей тут может быть много, даже не знаю, на какой из них больнее смотреть.
Это не все! Есть еще третья непростительная идиома, куда страшнее их всех...
A suivre.
😁7😎3😱2
explicit instantiation
ПОСТОЯННАЯ БДИТЕЛЬНОСТЬ!
Если вы думаете, что с++ непогрешим, то спешу вас расстроить. Я могу такого порассказать, что у вас волосы будут шевелиться в самых нескромных местах! Всякими нестыковками и лазейками в стандарте спешат воспользоваться ушлые и небрезгливые ребята, а потом еще и хвастаются этим на профильных конференциях.
Они говорят: "Да, мы нарушаем инкапсуляцию, но только ради тестирования". Однако мы знаем правду... дай им волю, эта зараза будет повсюду.
Вот, в один несчастный день кто-то все же осилил стандарт целиком, а возможно наткнулся случайно на параграф 14.7.2 Explicit instantiation [temp.explicit] абзац 12:
Обычные правила проверки доступа неприменимы к именам в при явном инстанцировании.

Хотя именно в этом разделе правило появилось не так давно, но и раньше его можно было найти в Template instantiation and specialization [temp.spec].
Интересно, почему эта конструкция получила такие послабления?
Пишут, что явное инстанцирование - штука полезная и может уменьшить время компиляции. Очень может быть. Тогда почему бы закрытому члену класса не воспользоваться этим? Если бы приватность не нарушалась, то это могло бы выглядеть следующим образом:
template <class T>
struct CoolFeature {
};
class Something {
int32_t value {40};
template struct CoolFeature<&Something::value>;
};

Однако такое описание некорректно. Возможно, причина в том, что стандарт настоятельно рекомендует использовать конкретное явное инстанцирование единственный раз в программе (14.7 Template instantiation and specialization [temp.spec]), поэтому размещать его в заголовочных файлах не самая лучшая идея, не говоря уже об описаниях класса.
Исходя из каких-то таких соображений и пришлось смиренно приоткрыть доступ к приватным переменным только для явного инстанцирования.
Вы же понимаете, чем это грозит?
Это огромная дыра, через которую криминальный ум сразу же утащит все ваши потаенные члены класса.
Например, имеется:
class Secret {
int32_t id {0x12345678};
int32_t code {100};
};

Сделаем специальный класс для кражи данных
template <int ID, class R, class C>
struct Thief {
using member = R C::*;
inline static member private_member {};
};

Выглядит невинно аки овечка: всего лишь определяем тип указателя на член класса member и объявляем статическую переменную этого типа.
Шаблонный параметр ID нужен только для придания неповторимости типу Thief, ведь для каждого члена класса Secret нужно определить свой класс.
using Secret_id = Thief<0, int32_t, Secret>;
using Secret_code = Thief<1, int32_t, Secret>;

Secret_id будем использовать для получения доступа к id, а Secret_code - для code.
Наконец, класс, который выдаст нам адреса, пароли и явки.
template <class T, T::member t>
struct Outlaw {
Outlaw(T::member private_member) { T::private_member = private_member; }
inline static Outlaw instance {t};
};

Этот класс свяжет член класса Secret и статический член класса Thief, через статический же объект instance. Очень удобно, что объект создастся при инстанцировании.
template struct Outlaw<Secret_id, &Secret::id>;

При явном инстанцировании Outlaw в шаблонных аргументах можно указать &Secret::id и ничего нам за это не будет. При этом вызовется конструктор Outlaw объекта instance, который перенесет указатель в статическую переменную Secret_id::private_member.
Такая вот нехитрая схема хищения, воспользуемся указателем:
std::cout << s.*Secret_id::private_member << std::endl;

Вот так можно получить доступ к приватным членам класса, и что самое страшное, без шума и пыли.
👍8🔥2🤷‍♂1
friend injection
"Если с другом вышел в путь, веселей дорога!" - говорит нам старинная советская песня. "Если ты упал и подняться не смог, друг под зад тебе отвесит пинок", - вторит ей другая композиция уже из нового времени. В любую эпоху дружба ценилась, ведь имея правильных друзей можно многого достичь, например, закрытых членов класса.
Идиома friend injection напрямую к криминалу отношения не имеет. Ее суть скорее в возможности использования снаружи класса дружественной функции, которая определена внутри.
Например:
struct Something {
friend void Print() {}
};

Тогда достаточно задекларировать ее
void Print();

и вызывать вне контекста класса:
Print();

Олдскулы подсказывают нам, что на этом механизме когда-то строился трюк Бартона-Накмана. Эта другая идиома, которая заготавливала дружественную функции внутри базового шаблонного класса.
Например, такой базовый класс:
template<typename T> class EqualComparable {
friend bool operator==(T const &a, T const &b) { return a.EqualTo(b); }
};

Наследуясь от него, мы получаем в качестве бонуса, т.е. на шару, оператор сравнения.
struct Something : private EqualComparable<Something> {
bool EqualTo(value_type const& rhs) const; // Только вот тут определить функцию и ништяк
};

Изначально трюк Бартона-Накмана эксплуатировал упомянутую особенность, и объявление дружественной функции внутри класса делало ее имя доступным в окружающем пространстве имён. Потом кое-кто ужаснулся такому положению дел, и правила поиска подшаманили, теперь такими функциями занимается ADL, и напрямую такой ::operator==(a, b) вызвать нельзя.

Однако возможность куражиться над иными кореш-функциями никуда не делась. А тут еще в с++20 появилось искушение использовать указатель на член класса как шаблонный параметр. Это значит, время усовершенствовать метод кражи закрытых данных.
template < int Secret::*Member >
class Outlaw {
public:
friend int& GetPrivateMember(Secret& obj) {
return obj.*Member;
}
};

Указатель на член класса передается Outlaw как шаблонный параметр. Функция-друг GetPrivateMember принимает как аргумент ссылку на класс Secret, и, применив Member, возвращает ссылку на член класса.
Теперь, чтоб получить доступ к приватным членам, достаточно только явно инстанцировать Outlaw:
template class Outlaw<&Secret::code>;

и декларировать функцию, которая обеспечит беспрепятственный доступ к телу:
int GetPrivateMember(Secret&);
Secret s {};
GetPrivateMember(s) = 42;


Выглядит неплохо, но нельзя же мириться с тем, что один класс Outlaw уходит только на один член класса.
Если модифицировать немного, добавить тэг и аргумент уникального типа в дружественную функцию, то возможно использовать этот Outlaw для вскрытия нескольких членов класса или даже разных классов.
template <auto, int Tag>
struct Outlaw;

template <class S, int S::* Member, int Tag>
struct Outlaw <Member, Tag> {
friend int& GetPrivateMember(Secret& obj, std::integral_constant<int, Tag>) {
return obj.*Member;
}
};

template struct Outlaw<&Secret::code, 0>;
template struct Outlaw<&Secret::id, 1>;

int& GetPrivateMember(Secret&, std::integral_constant<int, 0>);
int& GetPrivateMember(Secret&, std::integral_constant<int, 1>);

Secret s {};
std::cout << "id " << GetPrivateMember(s, std::integral_constant<int, 1>{}) << std::endl;

Хотя, конечно, так делать не стоит. Никогда!
🔥3🤷‍♂2👍1💊1
abuses of access rights
Опосля последней публикации пришел ко мне некто Саттер в виде астральном, но смотрел с такой укоризной, что хотелось забиться под плинтус. "Что же ты, собака, про меня ничего не сказал, - говорил он устало, - я тоже много всяких извращений описал лет 15 назад".
"Так ведь все они супротив friend injection как плотник супротив столяра", - пищу я, забившись в угол.
Тут он бросает в меня zero-cost исключение, и я в ужасе просыпаюсь. Передо мной на дисплее светится заметка Герба Саттера "Uses and Abuses of Access Rights", в которой рассмотрены методы нарушения инкапсуляции разной степени отвратительности. Парочку мы уже случайно рассмотрели, остались еще два любопытных способа.

Способ "Врун":
Пусть в заголовочном файле находится описание класса Secret.
// secret.h
class Secret {
int32_t id;
int32_t code;
public:
int GetCode() const;
};

но, например, в main.cpp мы не подключаем secret.h, а пересоздаем описание класса:
class Secret {
int32_t id;
int32_t code;
public:
int GetCode() const;
friend void Hijack(Secret&);
};

Мы добавили в описание дружескую функцию Hijack, где будем сможем менять значение приватных членов.
void Hijack( Secret& x ) {
x.code = 42; // злодейский смех
}

Итого:
Secret x{};
Hijack(x);
std::cout << "id " << x.GetCode(); // 42!

Это противозаконно, поскольку нарушает One Definition Rule. Ведь если тип определен более одного раза, определения должны быть хотя бы идентичны.
Хотя и в этом случае, даже если класс будет называться так же, может даже выглядеть так же, но это не он, это ловкий врун.

Способ "Дровокат".
Обычно методы, ломающие правила инкапсуляции, воняют, но некоторые источают аромат меньше других.
Допустим, наш класс заполучил какую-то шаблонную функцию.
class Secret {
int32_t id;
int32_t code;
public:
template <class T>
int get(T t) { return 0; }
}

Тогда мы можем специфицировать ее!
namespace {
struct Y {};
}
template<>
int Secret::get(Y const&) {
code = 42; // злодейский смех
return 1;
}

Secret x {};
x.get(Y{}); // code == 42

Если бы метод был человеком, то этот был бы плюсовой шапиро, ловчила-законник, кто знает лазейки. Его не поймать на горячем, он соблюдает букву закона, но полностью выхолащивает его дух. Ведь специализировать шаблон для любого типа разрешено. Ошибка может возникнуть, если бы мы попытались специализировать метод для одного и того же типа разными способами, что будет нарушением ODR, но это можно обойти.
Здесь код использует тип, который гарантировано уникальный, потому как находится в собственном неименованном пространстве имен.
А как вам такое, мистрер Саттер? Если у нас есть просто шаблонный класс:
template <class T>
class Secret {
int32_t id {0x12345678};
int32_t code {100};
public:
int get() { return 0; }
};

Такую функцию тоже можно специализировать:
template <>
int Secret<int>::get() {
code = 42; // злодейский смех
return 1;
}

Взять, к примеру, знакомый всем std::array. Сейчас я легким движением руки выпотрошу его внутренности:
template <>
int& std::array<int, 10>::front() noexcept {
this->_M_elems[0] = 42; // злодейский смех
return *begin();
}

Это даже работает. Не знаю, зачем я это все описал. Забудьте и никогда не используйте.
👍53🤷‍♂1
сompile-time сounter
Еретики из секты извращенных попрателей плюсов давно нашли богохульное и интересное применение friend injection. Они строят на основе этого эффекта богомерзкий счетчик, что во время компиляции возвращает число обращений к нему. То есть, по сути, может генерировать уникальные номера времени компиляции. Код выглядит безумно, как картины Босха, однако это изощренный способ облапошивания простодушного GCC:
template<int N>                      
struct Flag {
friend constexpr bool flag(Flag<N>);
};

template<int N>
struct Writer {
friend constexpr bool flag(Flag<N>) {
return true;
}
static constexpr int value = N;
};

template<int N = 0>
constexpr int reader(float) {
return Writer<N>::value;
}

template<int N = 0,
bool = flag(Flag<N>{}),
auto = []{}>
constexpr int reader(int) {
return reader<N + 1>(int{});
}

template<int R = reader<0>(int{})>
constexpr int next() {
return R;
}

Пример, наглядно иллюстрирующий работу функции next:
static_assert(next() == 0);
static_assert(next() == 1);
static_assert(next() == 2);

Отвратительное и одновременно завораживающее зрелище.
Как вы могли заметить, next - шаблонная constexpr функция с единственным шаблонным аргументом R, у которого есть значение по умолчанию. Значение это дает шаблонная же функция reader, которую мы вызываем с шаблонным аргументом 0 и обычным аргументом int{}.
Знатоки метапрограммирования сразу поймут, что функция reader рекурсивная, если она инстанцировалась успешно, то пытается вызвать себя же с инкрементированным шаблонным аргументом. Соответственно, неуспех инстанцирования определяет конец рекурсии. Функция reader будет создана только в том случае, если в глобальном пространстве имен присутствует определение функции flag(Flag<N>). Изначально у нас нет ни одного определения функции flag, поэтому компилятор сватает нам низкоприоритетную функцию reader<0>(float), которая нам не очень подходит из-за необходимого неявного преобразования int во float. Однако выбирать не приходится, другой функции у него для нас нет. Зато reader<0>(float) инстанцирует внутри себя шаблонную структуру Writer<N>, в которой будет определена функция flag(Flag<0>)!
Тогда при повторном вызове функции next в глобальном пространстве имен уже будет находиться функция flag(Flag<0>), поэтому функция reader<0, flag(Flag<0>{}), lambda> таки будет создана, но вот на создании reader<1, flag(Flag<1>{}), lambda> компилятор снова споткнется и запросит помощи у reader<1>(float), который вернет единицу и создаст определение для flag(Flag<1>) и так далее. Принцип понятен.

У функции reader есть немаловажная деталь в шаблонных аргументах - третий аргумент должен генерировать значение уникального типа, поэтому по умолчанию там лямбда. Каждая лямбда имеет уникальный тип, не верите? Проверьте:
static_assert(not std::is_same_v<decltype([](){}), decltype([](){})>);

Это нужно, чтоб при инстанцировании шаблона контекст каждый раз оценивался заново, что и дает возможность во время компиляции "менять" поведение функции flag.
Ну, красиво же надурили! Однако не надейтесь, что единожды воспользовавшись слабостью компилятора, вы будете получать дивиденды вечно.
Вот и ликование святотатцев длилось недолго: GCC 12 версии и выше пресекает подозрительные действия, и конкретно этот пример теперь не работает.
👍6👾3🔥21
сompile-time сounter. next
Этот пример счетчика был практически хрестоматийным. Я встречал его множество раз. Находил всюду его следы. Почему же пал этот древний титан? Вопрос терзает меня будто ночной комариный писк. Не отмахнешься просто так, он засел занозой в голове. Он не дает спать. Раннее утро, но я уже давно на ногах, всем своим помятым сознанием соображаю, по каким причинам произошел эпичнейший провал. Город за окном еще сладко дремлет, залитый робкими рассветными лучами. Я напряженно вслушиваюсь в хрупкую, еще ночную тишину. Пока звук сирен не разрезал безмолвие нарождающегося дня, время еще есть.
Допустим, разработчики компилятора сломали прошлый пример намеренно, чтоб в разработке был порядок. Ведь нельзя же строить код вокруг лакуны стандарта или же убербага компилятора, иначе в одно прекрасное утро проект с грохотом рухнет. Однако это не остановит извращенные умы, да здравствует stateful metaprogramming!
Что же сломалось при переходе к новой версии компилятора? Рассмотрим первый вызов функции next: до поры все работает как надо, функция flag(Flag<0>) не будет найдена, и мы обратимся к reader<0>(float), получим заслуженный ноль и определение функции flag(Flag<0>) в глобальном пространстве имен. Следующий вызов next не приводит к желанному эффекту. Компилятор будто не переоценивает заново контекст, игнорирует новое определение функции flag(Flag<0>), хотя оно точно появилось.
Компилятор как будто запомнил, что функцию reader<0, flag(Flag<0>{}), lamda> собрать невозможно и надо использовать reader<0>(float). Или ему просто лень. Он и не пытается даже зедекларировать нужную функцию повторно, и уникальный тип по умолчанию в шаблонных аргументах нам не очень помогает.
Что же, нас в дверь, а мы в окно. Нагло соврем, что компилируем вообще другую функцию, т.е. прямо укажем компилятору
template<int N = 0, auto = []{}>
constexpr int reader(float) {
return Writer<N>::value;
}
template<int N = 0, auto = []{},
bool = flag(Flag<N>{})>
constexpr int reader(int) {
return reader<N + 1>(int{});
}
template<int R = reader<0, []{}>(int{})>
constexpr int next() {
return R;
}

Чтоб не дать GCC вывернуться, внесем значение уникального типа прямо в вызов reader, тогда будет очевидно, что нужно инстанцировать функцию reader заново.
Счетчик снова работает во времени компиляции, древнее зло опять пробудилось! Я удовлетворенно откинулся в кресле и закрыл глаза.
Резкий звук ворвался в сонный полдень. Сирены! Они уже близко. Стук в дверь. Полиция плюсов! "Отворяй, собака! Тут совершается мыслепреступление...."
Опять по рукам будут бить.
👍71
compile-time. endgame
Инда взопрели озимые. Рассупонилось солнышко, расталдыкнуло свои лучи по белу светушку. Глянул старик Ромуальдыч портянку кода и аж заколдобился... а все потому, что XXI век на дворе, с++20 давно уже бороздит просторы Большого театра, а вы тут все со SFINAE играетесь. В 2к25 это должно быть постыдно. Надо же делать понятно, настолько доходчиво, чтоб уразумел распоследний зумер.
Поэтому перепишем счетчик еще раз в духе нового времени, с новыми фичами и соответствующим вайбом.
template<unsigned N>
struct reader {
friend consteval auto flag(reader<N>);
};

template<int N>
struct Writer {
friend consteval auto flag(reader<N>) { return true; }
static constexpr int value {N};
};

template <auto Tag, int Value = 0>
[[nodiscard]]
consteval int counter_impl() {
if constexpr (requires(reader<Value> r) { flag(r); }) {
return counter_impl<Tag, Value + 1>();
} else {
return Writer<Value>::value;
}
}

template<auto Tag = []{}, int Val = counter_impl<Tag>()>
constexpr int counter {Val};

Кода будто стало по противного мало, что наверняка сделало его понятнее. Или нет?
Работает счетчик так же:
static_assert(counter<> == 0);
static_assert(counter<> == 1);
...

Как вы могли заметить, теперь счетчик - это, простите за выражение, шаблонная константа. Первый аргумент шаблона Tag - это значение уникального типа, логично, что роль эта отводится лямбда-функции. Вторым аргументом шаблона следует значение Val - непосредственно значение счетчика. Получаем мы это значение через функцию counter_impl, тоже шаблонную, куда опять же передается уникальный тэг. Сделано это все с той же прагматичной целью: переоценивать контекст при каждом вызове.
Внутри counter_impl мы проверяем явно возможен ли вызов функции flag с аргументом типа reader<Value>,
if constexpr (requires(reader<Value> r) { flag(r); })

где Value - это второй агрумент шаблона, по умолчанию нулевой, стартовое значение счетчика. Если такую функцию flag вызвать сейчас вызвать невозможно, то мы идем в ветку, где явно инстанцируем Writer<Value>, чтоб функция появилась.
Иначе мы рекурсивно вызываем counter_impl, увеличивая каждый раз значение счетчика - counter_impl<Tag, Value + 1>().
Принцип все тот же, только записан как-то душевнее, человечнее, что ли.
Writer и reader почти не претерпели изменений, они все такие же пострелята. Однако разница есть. Тип возвращаемого значения функции flag указан как auto, и это важный момент, о котором авторы метода стыдливо умалчивают.
Дело в том, что если прямо указать возвращаемый тип void flag(reader<N>), то оценка
 
requires(reader<Value> r) { flag(r); }

будет положительной, несмотря на отсутствие реализации этой функции, и мы провалимся в бесконечную рекурсию. С auto все будет не так однозначно. Компилятор не может вывести возвращаемый тип без подглядывания в реализацию, и requires не хватает данных о возможности вызова, и оценка становится отрицательной. При инстанцировании Writer<Value> появляется реализация flag, информации снова достаточно, и оценка становится положительной.
Вот такая загогулина, понимаешь, но вы же серьезные люди и не будете пользоваться всеми этими сомнительными трюками?
👍7🤷‍♂2
nothing is what it seems
Как писал Конфуций в общедомовом чате: "Всё не то, чем кажется и не наоборот".
Давно бы пора запомнить, что нельзя смотреть пулл реквесты, если не просят. В код коллег лучше не подглядывать, чтоб сохранить хорошие рабочие отношения. Впрочем, если из кода летят предупреждения компилятора как из рога изобилия, то это явное приглашение. Он как бы кричит тебе: "Приезжайте и соответствуйте!". Вот я и заехал и сей же час поседел во второй раз. Есть у надежного кодирования свои особенности, например, архиважно инициализировать все переменные, но некоторые товарищи делают это весьма оригинальным манером:
struct NiceClass {...};

auto x = NiceClass();

Есть такое ощущение, что запись разоблачает в разработчике постыдное пристрастие к питону. Оно понятно, что хотел сказать автор, хоть для этого может потребоваться чуть больше когнитивных усилий, но после такого теряется концентрация. Как тут погружаться в код, если все время возвращаешься к этим строчкам и думаешь: "Ну вот зачем он это сделал?".
Есть ли еще недостатки у такого подхода, кроме жутковатого вида?
Запись эта теоретически допускает неоднозначность. Например, попытка вызова конструктора NiceClass() уж больно похожа на вызов функции. "Ну уж нет, такого быть не может, компилятор не допустит!" - возмущенно возразит тот, что еще не сталкивался с коварством GCC.
Впишем после определения класса функцию:
int NiceClass() {return 100;}

И теперь выполним еще раз
auto x = NiceClass();
static_assert(std::is_same_v<int, decltype(x)>); // OK, x == 100

Эта функция "затмит" имя класса и будет вызвана вместо конструктора.
Еще NiceClass может оказаться функтором сам по себе:
struct NiceClass {
int operator ()() const {
return 42;
}
}

Само по себе это не страшно, пока кто-то, например я, из хулиганских побуждений не создаст глобальную переменную
NiceClass NiceClass;

Тогда вызов
auto x = NiceClass();
static_assert(std::is_same_v<int, decltype(x)>); // OK, x == 42

ничто иное как вызов функтора, вызов перегруженного оператора ().
Конечно, пользователь может обезопасить себя, изобретая еще более уродливые конструкции, не дающие вызвать ничего кроме конструктора
auto x = std::decay_t<decltype(std::declval<NiceClass>())>();

тут в declval ожидается тип и подстановка иной сущности приведет к ошибкам компиляции, но выглядит это еще уродливей.
Самые обычные языковые конструкции, которыми хотят удивить, запуская их в небо под разными углами, могут дать вовсе неожиданный результат. Поэтому в AUTOSAR есть простое и понятное правило:
A8-5-2 При инициализации переменных должна быть использована скобочная инициализация {}, без знака присваивания.
В этом есть сермяжная правда. Делай проще, используй zero initialization, тогда твои намерения будут явными, и ничего страшного не случится.
NiceClass x {};

Здесь конструктор по умолчанию будет вызван, если он есть.
Потом взгляд мой упал на следующую строчку кода:
NiceClass t = decltype(t)();

я упал и забился в конвульсиях.
👍74🤷‍♂1😁1🎃1
static operator()
Разные есть у людей увлечения. Кто пляшет, кто чайный гриб выращивает, а кто и функторы в коде разводит. Особенно хорошо они во всяких алгоритмах и ranges размножаются. Бывало, идешь по коду, приподнимешь какой-нибудь transform, а функторы так и разбегаются в разные стороны. Бесспорно, эта мелочь небесполезна. Кинешь одного такого в какой-нибудь хитрый генерализованный алгоритм, он там пищит смешно, когда его за operator() дергают, зато входные данные обработаны универсальным способом.
Однако есть и недовольные текущим положением вещей. Например Barry Revzin и Casey Carter еще в октябре 2018 года выкатили предложение P1169R0, где слезно умоляли разрешить operator() быть статическим членом класса. Жаба, говорят, душит платить еще и за вызов метода класса.
Заглянем внутрь нашему общему знакомому, функтору std::less:
template<typename _Tp>
struct less : public binary_function<_Tp, _Tp, bool> {
bool operator()(const _Tp& __x, const _Tp& __y) const {
return __x < __y;
}
};

Очевидно, внутри оператора сравниваются два значения, попавшие туда в виде аргументов. В тот же метод неявно передается и указатель this, который здесь нужен как собаке пятая нога.
Поэтому, будь operator() обычным методом класса, особо рьяный анализатор уже бы выл о том, чтоб сделать его статическим.
struct X {
bool operator()(int) const;
static bool f(int);
};

Мы можем передать в алгоритм статический метод bool (X::f)(int)
std::count_if(xs.begin(), xs.end(), X::f);

Однако для функтора передача обязана идти через создание "материального" объекта.
inline constexpr X x;
std::count_if(xs.begin(), xs.end(), x);

Предложение разработчиков ranges даром не прошло, и уже в стандарте c++23 двум операторам дозволено быть статическими: operator() и operator[].
Теперь less можно переписать
template <typename T>
struct less {
static constexpr auto operator()(T const& x, T const& y) -> bool {
return x < y;
};
};

Но не надейтесь вызвать оператор как less<int>(), этого не будет, это по-прежнему вызов конструктора.
Вызвать оператор опять же можно напрямую
less<int>::operator()(1, 2), 

либо создав объект
less<int>{}(1, 2)

Зато вычислено это будет во время компиляции:
static_assert(less<int>{}(1, 2));

Еще одно важное следствие этого изменения связано с особым типом функторов - лямбдами.
Как вы знаете, все есть класс. Стандартные типы, вроде std::function - класс, std::array - структура вообще-то, но и структура - это класс, и разсахари (т.е. избавь от синтаксического сахара и перепиши в классическом представлении) лямбду, получишь класс. Раз уж мы разрешаем статические операторы для классов, то это должно повлиять и на лямбды. Теперь для лямбду без захвата можно пометить как статическую. Ну и правильно, зачем вам захват, если operator() будет статический и там не будет доступа к захваченным объектам?
Выглядит это примерно так
auto isEven = [](int i) static {return i % 2 == 0;};

От стандарта к стандарту лямбды становятся все прекраснее и быстрее! Но это неточно.
👍7🔥41
fmt consteval trick
Дорога в Болдино была невероятно красива и настолько же утомительна. Дабы разнообразить нашу постылую жизнь, автобус неожиданно принимался подпрыгивать и трястись, но водитель упорно не хотел сбавлять скорость, торопясь на встречу с прекрасным. Разморенный солнцем и убаюканный внезапно начавшейся ровной дорогой, я было задремал, как вдруг заметил, что в мою сторону крадется странного вида парень. Всклокоченные волосы удачно подчеркивали легкое безумие в глазах. Беззастенчиво продемонстрировав всем весьма потертую футболку с надписью "I eat c++ for breakfast", болезный упал на соседнее кресло.
- Нет, ну ты видел, что они сделали в fmt? - подмигнул он мне.
"Сумасшедший!" - мгновенно понял я, а вслух сказал:
- Аск! Во дают, да?! Ловкачи...
Хотя и понятия не имел, что они там удумали в своем fmt. Однако ехать было еще долго, а ретироваться через окно я счел ниже своего достоинства. Поневоле пришлось прислушиваться к незваному просветителю. Оказалось, что есть такие штуки, как спецификаторы формата. Вещь очень полезная для управления отображением данных во время форматирования. Обычно это символ или набор символов, например в printf написал %02x и сразу понятно, что мы хотим вывести число в шестнадцатеричном виде. Стандартным функциям хорошо, за них может и компилятор вписаться, кинет предупреждение, если тип переменной не соответствует спецификатору. Только за ваши велосипеды администрация ответственности не несет. Передали переменную не того типа, получите ошибку в run-time или вообще UB.
В GCC есть хотя бы расширения, атрибут формата
int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3)));

Чтобы компилятор проникся и понял, что my_printf он такой же как printf, только лучше.
Нам этого мало. Чаще всего и спецификаторы формата, и типы переменных известны уже на этапе компиляции, и неплохо бы там же диагностировать несоответствие, а не ждать, пока приложение погибнет в корчах. Вот, ходят слухи, что до такой проверки додумались в fmtlib. Рассмотрим некий упрощенный вариант, функцию fmt, которую корректно можно вызывать только так:
fmt("int", 10);

а такой вызов бы давал ошибку компиляции:
fmt("float", 10);

и вот такой тоже:
fmt("int", "foo");

В этом нам поможет новый инструмент c++20 - consteval. Он такой же, как и constexpr, только лучше и не обязан вкалывать после смены, т.е. в run-time.
Представим fmt в виде
template <typename T>
void fmt(std::type_identity_t<Checker<T>> checked, T) {...}

где первый аргумент мы через type_identity_t принудительно попытаемся привести к Checker<T>. Зачем? Все просто
template <typename T>
struct Checker {
consteval Checker(const char* fmt) {
if (fmt != std::string_view{"int"})
throw;
if (!std::is_same_v<T, int>)
throw;
}
};

Перед нами структура Checker, consteval спецификатор конструктора которой намекает, что неплохо бы исполнить его во время компиляции. Внутри конструктора мы проверяем "строку форматирования", и если она не "int", то разрушаем построение вызовом throw, которого не может быть в compile-time.
Далее мы проверим переданный в качестве шаблонного аргумента типа переменной T. Если он не int, то сигнализируем об ошибке тем же макаром. Использование конструктора Checker в качестве проверяющего дает нам самые широкие полномочия по сопоставлению строковых спецификаторов и типов.
Наконец мы добрались до точки назначения. Окрыленный новым знанием, я выбежал из автобуса.
- Хай, подруга! Ты представляешь, что они наделали в своем fmt? - завопил я, завидев перед собой юную барышню с томиком Онегина.
Глаза потом жгло невероятно после метко пущенной перцовой струи.
👍6🤔1
coder market interview
Солидные бородачи в свитерах с оленями сидели на массивных деревянных скамьях вокруг величественного шатра. Рядом высились и другие, но попроще, скорее похожие на палатки, и народ вокруг них сидел прямо на земле, ежась от холода в легкомысленных футболках и хмуро поглядывая на проходящих господ.
- Это и есть рынок разрабов, - пояснил я своему спутнику, агенту одной известной компании.
Он озирался по сторонам с интересом, я же смотрел только на раскинувшийся перед нами знакомый до боли шатер. Он все еще хорош, хотя раньше был великолепен. Узнаваемое созвездие на его полотнище когда-то было выложено бриллиантами, сейчас же их заменили светодиодами.
- Вылезай, Негоро! Тут телеком сеньоров ищет! - крикнул я, откинув полог.
- Я не Негоро! - около нас мгновенно материализовался подтянутый седовласый господин в дорогом костюме, - Себастьян Перейра, СЕО. Торговец естественным интеллектом! У нас все сеньоры, породистые, не с курсов! Сильные, даже не сомневайтесь!
- Не сомневаюсь, но хотел бы проверить... - робко сказал мой подопечный и достал из кармана бумажный листочек.
- Доми-Тори, сюда! - коротко скомандовал разработорговец.
Дюжий разраб медленно приблизился и с усталым вздохом развернул полученный листок.
Вверху было напечатано:
std::function<void()> make_printer() {
int counter = 0;
return [counter]() mutable {
++counter;
std::cout << counter << " ";
};
}

- Элементарно... - произнес Доми-Тори и ровным голосом продолжил.
Функция make_printer возвращает std::function, внутри которой мы поместим лямбда-функцию. Лямбда эта захватывает локальную переменную counter по значению, разумеется. Иначе это был бы epic fail. Внутри функции мы увеличиваем счетчик counter и выводим получившееся значение. Думаю, печать комментировать не нужно, а остальное есть суета и томление духа.
Достаточно вспомнить, что лямбда - это просто класс.
struct lambda {
int counter;
lambda(int counter) : counter {counter} {}
...

Тогда захват по значению сводится к передаче оного через конструктор переменной counter, члену класса по совместительству.
Внутри лямбды мы инкрементируем counter исключительно как член класса, поэтому нам просто необходим спецификатор mutable. Он избавит нас от навязчивого признака const, которым отмечен operator() любой лямбды по умолчанию.
void operator()() /*const*/ {
++(this->counter);
...
}

Доми-Тори умолк. В глазах покупателя забрезжила надежда.
- Что же выведет код? - тихо спросил он.
Разраб посмотрел на вторую половинку листа:
  auto printer = make_printer();
printer();
printer();

auto another_printer = printer;
another_printer();
printer();

- Вначале мы просто создаем объект printer функцией make_printer. Член класса counter получил значение ноль. Вызов функтора printer увеличит счетчик на единицу и ее же выкинет в стандартный вывод. Второй вызов доведет счетчик до двух.
Затем мы копируем лямбду, полагаю, не надо пояснять за копирующий конструктор по умолчанию. Значение counter объекта another_printer будет перенесено из printer, т.е. начнется с двоечки.
Соответственно, вызов another_printer доведет счетчик до трех, но и вызов printer сделает то же самое. Поэтому на экране мы увидим: 1 2 3 3
- Чудесно! - представитель компании захлопал в ладоши. - Беру! Беру всех!
После заключения сделки Перейра и агент хитро улыбались, каждый был абсолютно уверен, что надул другого.
- Зачем вам столько безвольников, проект сложный затеяли? - спросил я напоследок.
- Ответственной работы очень много! Тесты на питоне кому попало не доверишь...
👍7🔥1
std::string_view
Как-то осенью поэт Басё и его ученик Кикаку шли по рисовому полю.
- Сегодня на ревью я увидел красивый код и сложил хокку, - сказал вдруг Кикаку, - если добавить к пейзажу веревку, то увидишь std::string_view.
- Нужно любить то, о чем пишешь, - ответил Басё и предложил улучшить стих. - Если добавить Кикаку мозгов, то получился бревно.
Тут поэт поймал красную стрекозу и задумчиво оторвал ей крылья, превращая оную в стручок перца.
- Когда ты уже запомнишь, что std::string_view нужно передавать по значению?!
Может, это был Басё, а может, и Arthur O’Dwyer, история умалчивает. Однако последний в своей небольшой статье прямо утверждает, что string_view и подобные объекты просто необходимо передавать по значению. Это исторически верно, ООП должен работать с объектами. Это концептуально правильно! Всякие там передачи по указателю или по ссылке придумали трусы, негодяи и оптимизаторы.
Прежде всего, вспомним, что string_view - это упрощенное представление строки, состоящее из указателя и длины. Идея прозрачна и понятна, как платье Бьянки Цензори. В стандартную библиотеку концепт попал в таком виде:
using string_view = basic_string_view<char>;

где basic_string_view - это шаблонный класс
template<typename _CharT, typename _Traits = std::char_traits<_CharT>>
class basic_string_view {
...
size_t _M_len;
const _CharT* _M_str;
};

Размер всего класса на нашей 32-битной arm платформе будет 8 байт.
Верно ли, что объект такого типа лучше передавать по значению?
Первой причиной можно назвать устранение косвенного обращения по указателю.
Передача константой ссылки значит, что, скорее всего, передается адрес объекта. Передача же значения - что объект передается непосредственно через регистры, если он достаточно субтильный.
Для примера сравним две функции. В одну мы передаем string_view по значению, во вторую - по ссылке. В обоих случаях возвращаем размер представляемой строки.
int byvalue(std::string_view sv) { return sv.size(); }
int byref(const std::string_view& sv) { return sv.size(); }

Результат работы компилятора ошеломляет. Для первого случая нам вообще ничего не придется делать.
byvalue(std::basic_string_view<char, std::char_traits<char>>):
ret

Объект передан через регистры, для его передачи использованы два регистра. В первый как раз попала переменная _M_len, которую нам нужно вернуть. Какая же удача, что через этот регистр мы получаем возвращаемое значение.
А вот в другом случае при передаче ссылки приходится поработать с памятью, что будет сложнее и дольше:
byref(std::basic_string_view<char, std::char_traits<char>> const&):
ldr w0, [x0]
ret

Погодите, что это за регистры? Это не наша архитектура, это 64-битный arm.
Неудобно получилось, ведь для arm32 картина будет иной.
byvalue(std::basic_string_view<char, std::char_traits<char>>):
sub sp, sp, #8
strd r0, r1, [sp]
add sp, sp, #8
bx lr

Видно, что объект пришел через стек, потом был записан в два регистра. Первым в std::string_view идет длина, она же и попадет в регистр r0, где и должно быть возвращаемое значение. На этом выполнение функции, в общем-то, можно и закончить.
Передача по ссылке:
byref(std::basic_string_view<char, std::char_traits<char>> const&):
ldr r0, [r0]
bx lr

Выглядит проще. Собственно, в более поздней статье Артур оправдывается, что результат работы зависит от конкретного компилятора и calling convention.
string_view считается большим типом и, что самое обидное - не фундаментальным. Если бы мы использовали uint64_t, компилятор бы разместил значение на двух регистрах, но похоже, сложные объекты он предпочитает передавать через стек, к сожалению.
Но это почти так же быстро, разница особо не чувствуется.
Можно, конечно, написать так:
int byvalue(uint64_t s) { return reinterpret_cast<std::string_view *>(&s)->size(); }

И мы получим желаемое:
byvalue(unsigned long long):
bx lr

Только вызывать такую функцию немного сложновато. Ладно, согласен, первый аргумент за передачу по значению слишком уж зыбкий, зависит от конкретной платформы и немного сомнителен. Надеюсь, остальные причины окажутся более убедительными. Проверим.
6👍31
std::string_view. part2
Наконец-то лето стало похоже на настоящее, а это значит, что пришло время отпуска! Его я решил провести в аэропорту Шереметьево. Просто я люблю спать сидя, дорого есть и стоять в длиннющей очереди на посадку, но только не садиться в самолет после получасового переминания с ноги на ногу, а резко сорваться к другому выходу. Какая-никакая, а физическая активность. Почти "веселые старты".
В очередной раз, когда нестройная колонна отдыхающих рассыпалась - рейс задержали еще на неделю, у меня появилось немного времени, чтоб таки вернуться к наиболее веским причинам передавать std::string_view по значению. Сейчас только пристроюсь с ноутом на освободившееся место у окна на стене. Лучше карабкаться повыше, иначе люди беспардонно головами задевают.
Так вот, хладнокровно рассмотрим ситуацию со стороны вызывающего.
Хотим мы передать некий объект по ссылке, и в большинстве случаев это будет значить, что положить в регистр придется адрес. Т.е. нужно записать это нечто в память, чтоб у него этот адрес вообще появился. Поэтому мы вынуждены порой задействовать стек. Даже если бы наш гипотетический объект уместился бы в регистре.
Допустим, есть две функции, одна принимает std::string_view по значению, другая - по ссылке.
int32_t byvalue(std::string_view sv);
int32_t byref(const std::string_view& sv);

Посмотрим, как будет выглядеть вызов этих функций в ассемблере. Для этого приготовим две функции: callbyvalue и callbyref. Они будет вызывать byvalue и byref соответственно.
void callbyvalue() { byvalue("hello"); }
void callbyref() { byref("hello"); }

Для архитектуры x86-64 GCC сгенерирует такой ассемблер:
.LC0:
.string "hello"
callbyvalue():
mov edi, 5
mov esi, OFFSET FLAT:.LC0
jmp byvalue(std::basic_string_view<char, std::char_traits<char>>)

Тут все очевидно, мы сразу кладем в регистры все что нужно и вызываем byvalue.
callbyref():
sub rsp, 24
mov rdi, rsp
mov QWORD PTR [rsp], 5
mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0
call byref(std::basic_string_view<char, std::char_traits<char>> const&)
add rsp, 24
ret

Тут немного сложнее. По сути, мы конструируем полноценный объект string_view на стеке и передаем его адрес в функцию.
Интересно, так ли это для архитектуры arm32? В прошлый раз результат был не очень. Что же, прогоняем код через соответствующий GCC и видим:
callbyvalue():
sub sp, sp, #8
movs r2, #5
movw r3, #:lower16:.LC0
movt r3, #:upper16:.LC0
strd r2, r3, [sp]
ldrd r0, r1, [sp]
add sp, sp, #8
b byvalue(std::basic_string_view<char, std::char_traits<char>>)

Очень похоже на результат для x86-64, но есть нюанс. Тут мы все равно задействуем стек, хоть оптимизация -O3 включена. Здесь мы конструируем объект string_view на стеке, затем выгружаем его обратно в регистры r0 и r1, чтоб передать их в руки byvalue.
callbyref():
push {lr}
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
sub sp, sp, #12
ldm r3, {r0, r1}
stm sp, {r0, r1}
mov r0, sp
bl byref(std::basic_string_view<char, std::char_traits<char>> const&)
add sp, sp, #12
pop {pc}

Результат для callbyref никаких неожиданностей не содержит. Мы сохраняем string_view на стеке, копируем адрес в регистр и вызываем byref.
Разница вроде бы незначительная, стек в любом случае используется, но ассемблер второго примера всегда длиннее. Хорошо, скрипя мозгом, согласимся с этим доводом, хотя он такой, "на тоненького". Следующий должен быть просто убийственный.
Тут пальцы ног предательски разжались, и я свалился со стены. Самое время постоять с важным видом в какой-нибудь очереди на посадку.
std::string_view. part2
Наконец-то лето стало похоже на настоящее, а это значит, что пришло время отпуска! Его я решил провести в аэропорту Шереметьево. Просто я люблю спать сидя, дорого есть и стоять в длиннющей очереди на посадку, но только не садиться в самолет после получасового переминания с ноги на ногу, а резко сорваться к другому выходу. Какая-никакая, а физическая активность. Почти "веселые старты".
В очередной раз, когда нестройная колонна отдыхающих рассыпалась - рейс задержали еще на неделю, у меня появилось немного времени, чтоб таки вернуться к наиболее веским причинам передавать std::string_view по значению. Сейчас только пристроюсь с ноутом на освободившееся место у окна на стене. Лучше карабкаться повыше, иначе люди беспардонно головами задевают.
Так вот, хладнокровно рассмотрим ситуацию со стороны вызывающего.
Хотим мы передать некий объект по ссылке, и в большинстве случаев это будет значить, что положить в регистр придется адрес. Т.е. нужно записать это нечто в память, чтоб у него этот адрес вообще появился. Поэтому мы вынуждены порой задействовать стек. Даже если бы наш гипотетический объект уместился бы в регистре.
Допустим, есть две функции, одна принимает std::string_view по значению, другая - по ссылке.
int32_t byvalue(std::string_view sv);
int32_t byref(const std::string_view& sv);

Посмотрим, как будет выглядеть вызов этих функций в ассемблере. Для этого приготовим две функции: callbyvalue и callbyref. Они будет вызывать byvalue и byref соответственно.
void callbyvalue() { byvalue("hello"); }
void callbyref() { byref("hello"); }

Для архитектуры x86-64 GCC сгенерирует такой ассемблер:
.LC0:
.string "hello"
callbyvalue():
mov edi, 5
mov esi, OFFSET FLAT:.LC0
jmp byvalue(std::basic_string_view<char, std::char_traits<char>>)

Тут все очевидно, мы сразу кладем в регистры все что нужно и вызываем byvalue.
callbyref():
sub rsp, 24
mov rdi, rsp
mov QWORD PTR [rsp], 5
mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0
call byref(std::basic_string_view<char, std::char_traits<char>> const&)
add rsp, 24
ret

Тут немного сложнее. По сути, мы конструируем полноценный объект string_view на стеке и передаем его адрес в функцию.
Интересно, так ли это для архитектуры arm32? В прошлый раз результат был не очень. Что же, прогоняем код через соответствующий GCC и видим:
callbyvalue():
sub sp, sp, #8
movs r2, #5
movw r3, #:lower16:.LC0
movt r3, #:upper16:.LC0
strd r2, r3, [sp]
ldrd r0, r1, [sp]
add sp, sp, #8
b byvalue(std::basic_string_view<char, std::char_traits<char>>)

Очень похоже на результат для x86-64, но есть нюанс. Тут мы все равно задействуем стек, хоть оптимизация -O3 включена. Здесь мы конструируем объект string_view на стеке, затем выгружаем его обратно в регистры r0 и r1, чтоб передать их в руки byvalue.
callbyref():
push {lr}
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
sub sp, sp, #12
ldm r3, {r0, r1}
stm sp, {r0, r1}
mov r0, sp
bl byref(std::basic_string_view<char, std::char_traits<char>> const&)
add sp, sp, #12
pop {pc}

Результат для callbyref никаких неожиданностей не содержит. Мы сохраняем string_view на стеке, копируем адрес в регистр и вызываем byref.
Разница вроде бы незначительная, стек в любом случае используется, но ассемблер второго примера всегда длиннее. Хорошо, скрипя мозгом, согласимся с этим доводом, хотя он такой, "на тоненького". Следующий должен быть просто убийственный.
Тут пальцы ног предательски разжались, и я свалился со стены. Самое время постоять с важным видом в какой-нибудь очереди на посадку.
👍4🤷‍♂1🔥1
std::string_view. endgame
Хоть мобильный интернет появился относительно недавно, я с содроганием вспоминаю времена, когда его не было. Вчера, например. Или позавчера. От безысходности даже залез в соседский сад, привлеченный слабым, но незапароленным сигналом wi-fi. Правда, на клацание клавиш выбежал старик Пропердыкин с берданкой. "Не ел я ваши яблочки, дедушка", - кричу я из-за куста крыжовника. А тот как завопит, что, мол, архаровцы-наркоманы-зумерки все его мегабайты сожрали, и давай солью палить. Еле ноги унес, но кое-что успел записать по поводу третьего и самого убедительного довода для передачи std::string_view по значению.
Передавать std::string_view по значению хорошо, ибо устраняет неопределенность. Срывает покровы. Ведь если передаешь некий объект по ссылке в функцию, то она знает об объекте далеко не все. Возможно, этот объект принадлежит кому-то еще? Поэтому оптимизировать итоговый код нужно очень консервативно и осторожно.
Передача по значению предоставляет более широкие полномочия для оптимизации.
Возьмем для иллюстрации две функции, вычисляющие длину переданной строки std::string_view. Естественно, найти размер можно и проще, но допустим, на меня напал приступ идиотии.
void byvalue(std::string_view sv, size_t *p) {
*p = 0;
for (size_t i=0; i < sv.size(); ++i) *p += 1;
}

В функцию мы передаем по значению строку sv, затем проходим счетчик от нуля до sv.size() и увеличиваем счетчик *p на каждом шаге. Значение возвращается через указатель.
void byref(const std::string_view& sv, size_t *p) {
*p = 0;
for (size_t i=0; i < sv.size(); ++i) *p += 1;
}

Вторая функция делает ровно то же самое, только sv передается по ссылке.
Казалось бы, нет никакой разницы в алгоритме, отличие только в подаче аргумента. Однако компилятор не согласен, и вот какой результат выдаст GCC ARM32, даже если выкрутим оптимизацию на O3:
byvalue(std::basic_string_view<char, std::char_traits<char>>, unsigned int*):
sub sp, sp, #8
str r0, [r2]
strd r0, r1, [sp]
add sp, sp, #8
bx lr

Внезапно компилятор прозревает, что мы хотим сделать столь неуклюжим способом, и безжалостно оптимизирует функцию, оставляя только сохранение длины sv по указателю p.
Судьба самого объекта sv его заботит мало: моя копия std::string_view, что хочу с ней, то и делаю. Иная картина вырисовывается для передачи по указателю.
byref(std::basic_string_view<char, std::char_traits<char>> const&, unsigned int*):
movs r3, #0
str r3, [r1]
ldr r2, [r0]
cbz r2, .L4
.L5:
adds r3, r3, #1
str r3, [r1]
ldr r2, [r0]
cmp r3, r2
bcc .L5
.L4:
bx lr

Если компилятор и понял идею, то не смеет оптимизировать код, объект sv функции не принадлежит. GCC включает дурака и с чистой совестью исполняет именно тот бред, что мы ему тут написали. Для x86-64 будет примерно то же самое.
Теперь понимаете, насколько продуман был AUTOSAR, предписывающий передавать по значению любой объект размером не больше 2*sizeof(void*). Эх, как много еще людей не знают истинного стандарта...
👍3
std::like_t
"Лето наконец-то перестало притворяться невинной весной и таки показало свой хищный оскал. Многие разработчики от этого попадали в отпуска. Еще бы, ведь использование плюсов повышает температуру тела, что в горячее время года может вызвать необратимую денатурацию белка...", - тут я в ужасе закрыл интернет.
Хватит рыться в с++, надо переходить в легкий жанр, детективные истории в квантовом мире... или в тревел-блогеры податься.
Я открыл путеводитель по Алтаю. На полях семнадцатой страницы карандашом было выведено: "Где std::like_t?". Я пытался игнорировать надпись и читать дальше про красоты Чулышманской долины, но занозой в мозгу засел вопрос: "Какой еще std::like_t?".
Я закрыл книгу и открыл интернет.
Нечто похожее появилось в стандарте c++23 - std::forward_like.
Описание к этому объекту дается такое:
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;

Возвращает ссылку на x, тип которой имеет те же свойства, что и T&&. То есть возвращаемый тип определяется так:
Если тип std::remove_reference_t<T> константный, тогда добавляем к возвращаемому типу const (итого получаем const std::remove_reference_t<U>).
Если T&& это lvalue ссылка, тогда возвращаемый тип тоже будет lvalue ссылкой (иначе это rvalue ссылка).
Ну и тип T должен быть ссылочным, без волюнтаризма.
Собственно, все эти нововведения вышли из предложения p0847R0 Deducing this в феврале 2018 года за авторством Gašper Ažman, Simon Brand, Ben Deane и Barry Revzin.
Это то самое предложение, которое подарило нам явную передачу this в методы класса. Очень полезная штука, когда нужно реализовать адаптированные под контекст вызова методы, например, оператор доступа к элементу контейнера.
struct accessor {
vector<string>* container;
decltype(auto) operator[](this auto&& self, size_t i);
};

Глянув предложение по диагонали, я вспомнил откуда взялся std::like_t. Предполагалось, что в стандарт войдут две метафункции:
like_t, переносящая все квалификаторы с первого типа на результирующий. (т.е. like_t<int&, double> выдает double&)
и forward_like, модификация std::forward, что передает свой аргумент с адаптацией типа. В общем-то, forward_like<T>(u) это сокращенная запись для std::forward<like_t<T,decltype(u)>>(u).
Эти функции должны были помогать нам правильно реализовать работу с явным this
decltype(auto) operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>((*container)[i]);
}

Думаю, многие уже сталкивались с проблемой, когда тип возвращаемого значения необходимо подстроить под this, которому ничего не стоит подхватить где-нибудь дополнительные квалификаторы. Сейчас приходится городить всякий boilerplate код, а forward_like сильно облегчит нам работу, понимание кода и его сопровождение.
Хотя до стандартизации доползла только передаточная функция, like_t рано сбрасывать со счетов.
Заглянем в недра GCC:
template<typename _Tp, typename _Up>
[[nodiscard,__gnu__::__always_inline__]]
constexpr __like_t<_Tp, _Up>
forward_like(_Up&& __x) noexcept
{ return static_cast<__like_t<_Tp, _Up>>(__x); }

Сопротивление живо! Функция like_t существует под именем std::__like_t. Пользоваться, конечно, нужно с осторожностью.
🔥6
where is std::like_t
Что же помешало, кроме нелепого названия, std::like_t войти в стандарт?
Стенограммы обсуждения мне не выдали, но есть такое ощущение, что этой метафункции просто не нашлось достойного применения, кроме как готовить возвращаемое значение для forward_like. Оригинальное же предложение Deducing this приводит такое обоснование:
Набросаем базовый класс B с методом get, который использует явный this, чтоб вернуть ссылку на член класса i типа int.
struct B {
int i {1};
template <typename Self>
auto&& get(this Self&& self) { return self.i; }
};

И тут на сцену врывается некий наследник B - коварный D со своим собственным членом i типа double.
struct D : public B {
double i {2.};
using B::get;
};

Тогда при вызове get от объекта типа D, обнаруживается тонкий момент: Self в методе get будет D, а не B. Функция get вернет ссылку на D::i. Вообще, авторы большой проблемы в этом не видят, просто люди должны помнить, что метод с явным this - просто статическая функция-член с удобным синтаксисом вызова.
Ежели мы хотим странного, придется явно уточнить, что именно мы возвращаем: return (self)::B.i или forward<Self>(self).B::i для большей точности.
Однако все это перестает работать, если наследование приватное!
struct D : private B {
double i {2.};
using B::get;
};

Все, доступа к переменным B из класса D больше нет. Тогда, говорят авторы, упихивать коленом нужно аккуратно, но сильно, чтоб не растерять квалификаторы.
return ((like_t<Self, B>&&)self).i;

Без сантиментов в си-стиле привести self к B и взять нужное.
Именно тут, думаю, комитет с полными от ужаса штанами сполз под столы, откуда уже предложил развидеть последний пример в обмен на исключение like_t.
Вот в полезности std::forward_like никто не усомнился.
Допустим, есть член класса сложного типа, вроде std::unique_ptr<int>, реализуем функцию get старыми дедовскими методами.
struct FarStates {
std::unique_ptr<int> ptr {new int};

auto&& get(this auto&& self) {
return *std::forward<decltype(self)>(self).ptr;
}
};

Так делать совсем не круто, потому что std::unique_ptr всегда разыменовывается в неконстантную ссылку.
В результате такой бессмысленный код компилируется:
FarStates const fs {};
fs.get() = 1; // Этого не должно быть

Теперь воспользуемся forward_like:
auto&& get(this auto&& self) {
return std::forward_like<decltype(self)>(*self.ptr);
}

А вот с этим выражением проблем быть не должно, вернее, компилятор все понял и настучал нам по рукам.
error: assignment of read-only location 'FarStates::get<const FarStates&>(fs)'

Теперь можно не беспокоиться за свои велосипеды.
4
std::get_temporary_buffer
В очередной раз роясь в мусо... то есть разглядывая предметы в антикварной лавке, я заметил пару небольших вещиц: std::get_temporary_buffer и std::return_temporary_buffer.
От самих названий уже веет архаикой. Если произнести их вслух, то эхом вернется отголосок прекрасной эпохи мечтателей. В это сложно поверить, но существовали они еще до с++11 стандарта.
Даже революция одиннадцатых плюсов на них не сильно повлияла, вид get_temporary_buffer остался почти неизменен:
template<class T>
std::pair<T*, std::ptrdiff_t> get_temporary_buffer(std::ptrdiff_t count) noexcept;

Как вы уже догадались по названию, функция запрашивает временный буфер. Если count нулевой или отрицательный, то ничего хорошего нам не видать. В противном случае могут выдать неинициализированное непрерывное хранилище для count объектов типа T.
Впрочем, могут и не выдать, удовлетворить прошение частично, поэтому возвращать приходится пару значений.
Первый элемент пары - указатель на начало выделенной области памяти. Второй элемент показывает для скольких объектов выделена память.
Да, похоже на оператор new, только память остается неинициализированной.
Хоть буфер вроде и временный, но RAII тут неприменим, не молились люди тогда на ООП. Предполагалось освобождать его явно функцией return_temporary_buffer:
template< class T >
void return_temporary_buffer(T *p);

Функция освобождает хранилище, выделенное ранее через get_temporary_buffer.
Особой популярностью эти штуки не пользовались, разработчики недоумевали, зачем оно нужно, если есть malloc и new, и в с++17 функции были объявлены устаревшими. Потом, в с++20, вообще удалены из стандарта. Можно открыть один из последних черновиков и обнаружить бедолаг в списке живых мертвецов: 16.4.5.3.2 Zombie names [zombie.names].
Однако к нам в руки попало письмо некого разработчика gcc, не будем называть имен, хотя это Jonathan Wakely. Он пишет, что хоть функции помечены устаревшими, но ничего сделать нельзя, они уже используется, хоть и не напрямую, в stl_algo.h. Давайте просто спрячем эти функции в глубине стандартной библиотеки и сделаем вид, будто все удалили.
Действительно, если подсунуть GCC код auto x = std::get_temporary_buffer<uint64_t>(4); с опцией -std=c++23, то получим только предупреждение
warning: 'std::pair<_Tp*, long int> std::get_temporary_buffer(ptrdiff_t) [with _Tp = long unsigned int; ptrdiff_t = long int]' is deprecated [-Wdeprecated-declarations]

Можно немного копнуть глубже в GCC и использовать иное пространство имен:
auto x = std::__detail::__get_temporary_buffer<uint64_t>(4);

Только теперь возвращается не пара, а просто указатель, что вполне обосновано, ведь реализация у него такая:
template<typename _Tp>
inline _Tp* __get_temporary_buffer(ptrdiff_t __len) _GLIBCXX_NOTHROW {
...
#if __cpp_aligned_new && __cplusplus >= 201103L
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
return (_Tp*) _GLIBCXX_OPERATOR_NEW(__len * sizeof(_Tp), align_val_t(alignof(_Tp)), nothrow_t());
#endif
return (_Tp*) _GLIBCXX_OPERATOR_NEW(__len * sizeof(_Tp), nothrow_t());
}

Внутри-то там new, кто бы мог подумать! Они должны были побороть new, а не примкнуть к нему...
Собственно, поэтому функции условно мертвые. Временный буфер был предназначен для эффективной оптимизации небольших запросов памяти, но нет свидетельств того, что это было достигнуто на практике. Хотя они старались.
👍2🔥1
inherited struct initialization
Причин моего пригорания было две. Во-первых, компания Пёстелеком. Раньше я относился к ней нейтрально, пока не решил стать ее клиентом. Началось все еще два года назад, когда мимо моего дома проложили новенький черный и блестящий кабель, на котором каждые сто метров красовались синие бирки с гордой надписью "Пёстелеком".
Когда же губернатор стал устраивать нам цифровой детокс, то стал я поглядывать на заветный кабель со все нарастающим вожделением.
В итоге, я махнул рукой на влажную репутацию конторы и подал заявку на подключение.
Заявка быстро ушла в работу, мне уже виделось как пёстелекомовские инженеры склоняются над картой, словно отцы народов, потирают широкие лбы, соображая, как лучше доставить мне интернет.
"Только ADSL", - таков был окончательный вердикт технического отдела.
Ладно, мы люди негордые, ведите сюда вашу медяху. Синие бирки на кабеле издевательски подмигивали.
На следующий день заявку аннулировали. Нет технической возможности, сказали в техподдержке.
Так и слышу, как пёстелекомовский инженер отбрыкивается: "Очумели?! Как я без мобильного интернета буду кабели обжимать?"
Не хотят работать и ладно, но сколько времени упущено, Ди Жэньцзе сам себя на отсмотрит...
Вторая вымораживающая от копчика до макушки вещь - злоупотребление структурами. Людей привлекает их открытость, они начинают использовать их странным для меня образом. Тонкая грань между структурой и классом находится в идеологическом поле. Структура должна использоваться только в качестве бездушного агрегатора типов. У класса же есть душа, он сложнее организован, при порождении объекта класса он начинает жить, вызывается конструктор и т.п.
Нет ничего плохого в структуре:
struct P {int x, y;};

Это агрегатор. Только потом кто-то пишет
return P{.y=1};

Это не ошибка даже, но четкие пацаны собирают с -pedantic -Wall -Wextra, поэтому сборка будет замусорена предупреждениями
warning: missing initializer for member 'P::x' [-Wmissing-field-initializers]

Да, безопасней всего инициализировать все. Можно пойти на хитрость и добавить в структуру значения по умолчанию: struct P {int x = 1, y;};
Не нравится? Используй designator-инициализацию на полную: return P{.x = 1, .y = 1};
Либо вообще не используй: return P{0, 1}
(и потом вспоминай сквозь слезы, какое число что значит).
Однако если нужно менять структуру, добавить новый член, например, то оба варианта потребуют правок всех инициализаций подобного типа объектов.
Еще один изъян обнаруживается, если вспомнить, как в С++ обошлись с наследием Си.
Была разрешена пустая инициализация:
struct {} s = {};  // C++ одобряет

Зато нельзя стало:
- менять порядок инициалиазции членов:
struct P a = {.y = 1, .x = 2};  // C++ не одобряет (не тот порядок)

- миксовать обычную и designator-инициализации.
struct P c = {.x = 1, 2};  // C++ не одобряет (миксование)

- использовать вложенную инициализацию напрямую
struct Np { P p; int x; };
Np np {.p.x = 0, .p.y = 1, .x = 1}; // C++ не одобряет (это не designator)

Правильнее и безопаснее вложенную инициализацию нужно делать так
Np np {.p {.x = 0, .y = 1}, .x = 1};

Вот, как вы помните, открытое наследование не снимает статус агрегата у структуры:
struct A { int x; };
struct B : A { int y; };

Формально, B - это агрегат, можно использовать designator-инициализацию.
На деле же B b {.y=0}; принесет с собой кучу предупреждений компилятора про missing initializer for member 'B::A'.
Оно и понятно, базовый объект A ничем не проинициализирован. Нельзя просто написать B b {{}, .y=0}; - как мы уже выяснили, это запрещенное миксование. Но и выцепить для designator-а базовую структуру нельзя, у нее нет явного имени. Значения по умолчанию тоже не спасают struct A { int x=0; }; компилятору все равно не нравится.
Остается только противная базовая инициализация.
B b {{}, 1};

Поддерживать такое, если в структуру включены довольно сложные классы или множество элементов, удовольствие ниже среднего.
Лучше уж сделать честные классы с конструкторами, чем поддерживать получившуюся агрегатную лапшу.
6🤷‍♂1
overload resolution
Когда начинаю бугуртить, глядя на код некоторых проектов, я еду гулять в нижнюю часть города. Там нет интернета, и вероятность написать лишнее крайне мала.
"Ах, — думаю я, проходя мимо памятника паровозу завода 'Красное Сормово', — ведь в конце концов кто знает? Может быть, так и надо. Не может же быть, чтоб разработчики использовали возможности нового стандарта просто так".
Вот, к примеру, делают люди какой-то обработчик команд, где у каждой свой тип:
struct Cmd1 {};
struct Cmd2 {};
struct Cmd3 {};

Времени было мало, реализован только отклик на одну команду. Раньше-то обработчик был бы скучнее, чем обои в виндовсе:
struct Handler {
Result handle(Cmd1) { return Ok; }
Result handle(Cmd2) { return NotImplemented; }
Result handle(Cmd3) { return NotImplemented; }
};

Сейчас же есть много способов реализовать это, глаза разбегаются. Есть же auto, почему бы не применить его здесь?
struct Handler {
Result handle(auto cmd) { return NotImplemented; }
};

Если раскидать сахарок, то внутри мы найдем старый добрый советский...
struct Handler {
template<class type_parameter_0_0>
inline Result handle(type_parameter_0_0 cmd) { return NotImplemented; }
};

Да, да, да, шаблонный метод класса.
Думаю, всем понятно, что при вызове handle(Cmd1{}) будет инстанцирован метод
struct Handler {
...
template<>
inline Result handle<Cmd1>(Cmd1 cmd) { return NotImplemented; }
};

Впрочем, не будем ждать, пока это произойдет, возьмем дело в свои руки. Переопределим свой обработчик для реализованной команды вне класса:
template <>
Result Handler::handle<Cmd1>(Cmd1 x) { return Ok; }

Согласен, это не слишком куртуазно. Да и вообще, метод handle любой тип принимает без разбора. Нехорошо.
Нужно ограничить диапазон передаваемых типов. Хорошо, что плюсы стали модные, сделаем так:
Result handle(auto x) requires(std::is_same_v<decltype(x), Cmd1> ||
std::is_same_v<decltype(x), Cmd2> ||
std::is_same_v<decltype(x), Cmd3>) {
return NotImplemented;
}
Result handle(Cmd1) { return Ok; }

А что? Одну команду мы все-таки реализовали. С одной стороны, появилась неоднозначность, непонятно, что мы хотим на самом деле от обработки Cmd1, однако нет. Код вполне легален, хотя многие гуру не рекомендуют смешивать перегрузку методов и шаблонные методы. Слабаки! Они просто не читали тот небольшой фрагмент стандарта, описывающий мучительный выбор перегруженной функции. Страниц 25 вроде, я бы сказал точнее, если бы дочитал. Начать стоит с 12.2.2 Candidate functions and argument lists [over.match.funcs], где описан процесс формирования списка всех функций, которые хоть как-то подходят для запрашиваемого вызова. Потом быстро пройтись по 12.2.3 Viable functions [over.match.viable], где из всего набора кандидатов выбираются наиболее подходящие. Полирнуть стоит 12.2.4 Best viable function [over.match.best] - назначение крайней функции, которая и будет отдуваться за всех.
Там английским языком написано, что можно сравнить функции-кандидаты F1 и F2, и F1 будет лучшей функцией, если она нешаблонная, а F2 - шаблонная, причем аргументы не нужно будет каким-нибудь особо вычурным образом. Понятно, компилятор просто обязан выбрать Result handle(Cmd1).
Вот если аргументы нужно приводить к variant
Result handle(std::variant<Cmd2, Cmd3>) { return NotImplemented; }

тогда при вызове handle(Cmd2{}) будет выбрана шаблонная функция. Вот если бы шаблонной не было... но это не наш метод!
Остальные типы стильно отсекаем:
Result handle(auto x) requires(!std::is_same_v<decltype(x), Cmd1> &&
!std::is_same_v<decltype(x), Cmd2> &&
!std::is_same_v<decltype(x), Cmd3>) {
return WrongCmd;
}

Хотя в стандарте написано, что у методов с эллипсом приоритет вообще ниже плинтуса, вызовется он, если вообще нет других вариантов, т.е. можно было бы этим воспользоваться
Result handle(...) { return WrongCmd; }

однако это тоже не наш вариант, тут нет духа новой школы!
👍5