tgindex

C++: Хроники Дурки🚑

описание

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

908
подписчиков

Лучшие посты

за три месяца
  • 5 июн.2 304 просмотров46 реакций21 пересылок

    Вот сколько я дурки повидал (у меня канал про это целый заведен так-то !!!) но с удивлением я осознал, что вот такая штука %:include <iostream> int main() <% int a<:3:> = <% 10, 20, 30 %>; std::cout << a<:1:>; %> Компилируется во всех комипляторах... Потому что язык заботливо сохранил диграфы — на случай, если ваша клавиатура из 1973 года. Пипец какая жесть.

  • 29 мая2 206 просмотров37 реакций6 пересылок

    Разбираем письма читателей. Нам прислали вот такой вот код: #include <memory> #include <iostream> namespace user { struct user_type {}; using my_type = std::shared_ptr<user_type>; void tie(my_type const&, my_type const&) { std::cout << "user::tie\n"; } void oups() { my_type t1; my_type t2; tie(t1, t2); } } // namespace user int main() { user::oups(); } Что в нем примечательного. Под gcc/clang у нас в консоль ничего не запишется. Program returned: 0 Для MSVC x64 запишется Program returned: 0 user::tie А для MSVC x86 вообще случится страшное: Program returned: 3221225595 Ну и чтобы не оставлять предложку совсем уж без изменений, я добавлю от себя немного дурки. Если в вызов функции добавить скобки: void oups() { my_type t1; my_type t2; (tie)(t1, t2); } То, внезапно, в gcc/clang мы тоже будем печатать строчку. А вот в MSVC x86 все еще будет возвращаться ненеулевой код....

  • 16 июн.1 803 просмотров27 реакций1 пересылок

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

  • 16 июн.1 764 просмотров28 реакций2 пересылок

    В разных новых (уже скоро будет 10 лет) стандартах иногда бывают мелкие но очень приятные изменения. Вот такой код #include <iostream> int main() { bool b = true; b++; std::cout << b; } Был валиден до С++17, но <source>:6:5: error: use of an operand of type 'bool' in 'operator++' is forbidden in C++17 Вот так вот.

  • 22 июн.1 761 просмотров29 реакций25 пересылок

    Повторяю пропавший пост. Он был о докладе Евгения Ерохина с прошедшего CppRussia. С моими пространными размышлениями о том, какие у него сложные зубодробильные доклады и как надо каждый слайд ставить на паузу и уходить получать доп образования, чтобы не терять контекст... А пример, который хотел у него подрезать для этого канала был такой: int fn(int a, int b) { int res = 0; asm volatile ("nop"); asm volatile ("nop"); asm volatile ("nop"); asm volatile ("nop"); if (a > b) { // проблемный бренч res = std::rand(); } res = std::max(res, a); return res; } Утверждается, что это пример из реальной жизни, а постановка четырех NoOperation смещает операцию так, что branch prediction работает сильно лучше и значительно улучшает производительность. Вот такая она оптимизация в 2026 году.... P.S. Если я кому-то кидал текст изначального поста - дайте знать. Там было сильно больше интересного, но больно уж мне этот пример понравился. P.P.S. А вообще его доклад (и другие доклады) рекомендую к просмотру хотябы из чуства мазохизма. Если у кого-то есть иллюзии, что он шарит в IT - самое время разочароваться в собственных знаниях :)

  • 25 мая1 754 просмотров30 реакций7 пересылок

    Сегодняшняя рубрика называется "обычный шаблонный код, который компилируется только после жертвоприношения". Что выведет вот этот код? cpp #include <iostream> #include <vector> template <class T> struct LoggedVector : std::vector<T> { void Dump() const { if (empty()) { std::cout << "empty\n"; return; } std::cout << "size = " << size() << '\n'; } }; int main() { LoggedVector<int> v; v.Dump(); } Иииииии... Правильный отвееееет..... Да, вы правы, он ничего не выведет. Упадет на ошибке компиляции: ``` error: use of undeclared identifier 'empty' ``` Небольшой отступ чтобы код под спойлером не бросался в глаза Что тут не так. На самом деле надо делать или так: void Dump() const { if (this->empty()) { std::cout << "empty\n"; return; } std::cout << "size = " << this->size() << '\n'; } Или вот так: using std::vector<T>::size; using std::vector<T>::empty; Ты literally видишь перед собой size() и empty(), они вот там, в базовом классе, рукой подать. Но компилятор такой: Нет. В шаблонах я сначала притворяюсь, что базового класса почти не существует. Особенно приятно при большом рефакторинге, когда меняешь не-шаблонный класс на шаблонный, а он потом в произвольных местах кода ломается...

  • 21 июл.1 685 просмотров39 реакций15 пересылок

    Те, кто со мной регулярно общаются, знают, что я ненавижу сраный move. И всю move семантику. И у меня даже готовился доклад про то, как я его ненавижу, с кучей веселых примеров, которые не приехали еще сюда в дурку. Где я собрал в кучку почему именно я его ненавижу, но там получилось очень много и очень неструктурированно, поэтому доклад до сих пор в разработке. Но вот один пример, который я выкину таки сюда. int main() { std::map<int, std::unique_ptr<int>> m; m.emplace(1, std::make_unique<int>(42)); auto p = std::make_unique<int>(100); auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p))); } Здесь классика классика. Мапа из чего-то в в unique_ptr. Ну кто так не делал (здесь должна быть ссылке на open-source пример, но у меня жопа горит). Дальше мы делаем emplace. Return value A pair consisting of an iterator to the inserted element (or to the element that prevented the insertion) and a bool value set to true if and only if the insertion took place. Ну и вот второй emplace с ключом 1 вернет false. А что же произойдет с p? Он занулится, потому что мы его мувнули в функцию. int main() { std::map<int, std::unique_ptr<int>> m; m.emplace(1, std::make_unique<int>(42)); auto p = std::make_unique<int>(100); auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p))); std::cout << std::boolalpha; std::cout << "inserted = " << inserted << '\n'; std::cout << "p is null = " << (p == nullptr) << '\n'; std::cout << "*it->second = " << *it->second << '\n'; } Выводит inserted = false p is null = true *it->second = 42 Тоесть в мапу значение не поместили, а из указателя оно пропало... Восхитительно. Притом что если делать std::move "на месте" такого не происходит. int main() { auto p = std::make_unique<int>(100); std::move(p); std::cout << std::boolalpha; std::cout << "p is null = " << (p.get() == nullptr) << '\n'; } p is null = false Да, это чинится тем, что мы убираем make_pair, но это в тестовом примере, где это легко заметить. И когда уже знаешь проблему. Я задолбался ловить такие приколы с move.

  • 3 июл.1 580 просмотров72 реакций39 пересылок

    Ой, я когда-нибудь брошу этот сраный язык. Давайте разберем по шагам. int main() { Box b; b->hi(); } Что тут происходит? Мы создаем объект типа Box, иииии... вызываем у него оператор ->, а у того, что он вернет вызывает функцию hi(). Логично? Логично! Смотрим, что такое у нас тип Box. struct Box { Proxy operator->() { return {}; } }; Ага, значит оператор выдает экземпляр объекта типа Proxy, и вот у него вызывается функция hi(). Логично? Логично. Смотрим, что такое структура Proxy. struct Proxy { Leaf* operator->() { static Leaf x; return &x; } }; В структуре Proxy функции hi() нет. Ошибка компиляции. Логично? А хрен там плавал. Уберите детей от мониторов. Вот этот код компилируется и выводит ok #include <iostream> struct Leaf { void hi() { std::cout << "ok"; } }; struct Proxy { Leaf* operator->() { static Leaf x; return &x; } }; struct Box { Proxy operator->() { return {}; } }; int main() { Box b; b->hi(); } Итак. У нас С++ разворачивает оператор -> до самого конца. Например сработает даже вот это: #include <iostream> struct Leaf { void hi() { std::cout << "ok"; } }; struct Proxy4 { Leaf* operator->() { static Leaf x; return &x; } }; struct Proxy3 { Proxy4 operator->() { return {}; } }; struct Proxy2 { Proxy3 operator->() { return {}; } }; struct Proxy { Proxy2 operator->() { return {}; } }; struct Box { Proxy operator->() { return {}; } }; int main() { Box b; b->hi(); } Я вот хочу куда-нибудь в шаблонный код загнать, пранкануть, таксказать, коллег.

  • 26 июн.1 557 просмотров31 реакций10 пересылок

    Мой дорогой друг вот с этого канала о С++: @thisnotes Прислал мне просто восхитительную вещь. Давайте посмотрим вот на этот код const std::vector<Obj>& vec = (argc > 1) ? std::vector<Obj>{} : ready_vec; Вот тут зашита просто удивительая проблема. Нет, это не ссылка на временный объект, потому что const продлевает время жизни ссылки. Интересный факт, что вот такой код #include <iostream> #include <vector> struct Obj { explicit Obj() {} Obj(const Obj&) { std::cout << "copy\n"; } }; int main(int argc) { auto ready_vec = std::vector<Obj>{}; ready_vec.reserve(1); ready_vec.emplace_back(); const std::vector<Obj>& vec = (argc > 1) ? std::vector<Obj>{} : ready_vec; return vec.size(); } Компилируется в 16 строк ассемблера, а вот если мы сделаем вот так: #include <iostream> #include <vector> struct Obj { explicit Obj() {} Obj(const Obj&) { std::cout << "copy\n"; } }; int main(int argc) { auto ready_vec = std::vector<Obj>{}; ready_vec.reserve(1); ready_vec.emplace_back(); const auto empty_vec = std::vector<Obj>{}; const std::vector<Obj>& vec = (argc > 1) ? empty_vec : ready_vec; return vec.size(); } Компилируется в 6 строк ассемблерного кода. А фокус в том, что тернарный оператор в этой строчке const std::vector<Obj>& vec = (argc > 1) ? std::vector<Obj>{} : ready_vec; должен оба аргумента конвертировать к одному типу. А временный объект к ссылке сконвертировать не получится, поэтому и второй аргумент к ссылке не может быть конвертирован, и создается временная копия вектора. Думаю, в комментариях он подскажет, был ли этот пример найден в продуктовой задаче...

  • 16 июл.1 461 просмотров30 реакций10 пересылок

    Приятно видеть, что C++ настолько тщательно следит за обратной совместимостью. Мы столько боли ради этой обратной совместимости пережили. И теперь вдвойне приятно видеть, что поменяться может семантика вообще всего что угодно. Например, вот это: decltype(auto) f(int&& x) { return (x); } static_assert(std::same_as<decltype(f(1)), int&>); Оно нормально скомпилируется в С++20, но упадет на static_assert в C++23. Тоесть не просто перестанет компилироваться, а тихой сапой скомпилируется "в другое". Насколько понимаю, связано вот с этим: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html Но меня уже разоблачили, плюсовик я не настоящий и гениальности очередного замысла могу не понять. Как хорошо, что у меня канал не образовательный а развлекательный 🙂

  • 10 июл.1 458 просмотров37 реакций10 пересылок

    Вместо дурки я сегодня продолжаю пересматривать доклад великолепного Константина Владимирова. Жду не дождусь, когда он появится в открытом доступе, чтобы поскидывать интересные таймкоды. Это будет легко, просто скриптом каждые две минуты написать.... Вместо дурки хочу поделиться интересной конструкцией, о которой до этого не знал, но почерпнул из того доклада. Пусть у нас есть класс: template<typename T> struct S { S(T) { std::cout << "S(T)" << std::endl; } template<typename Iter> S(Iter beg, Iter end) { (beg); (end); std::cout << "S(Iter beg, Iter end)" << std::endl; } }; Если мы напишем что-то такое: int a = 3; auto s1 = S(a); static_assert(std::same_as<decltype(s1), S<int>> ); То оно нормально скомпилируется. А вот если мы попробуем как-то тригернуть второй конструктор: std::vector<double> v; auto s2 = S(v.begin(), v.end()); То ожидаемо упадем с ошибкой, смысл которой сводится к тому, что компилятор не знает, какой именно тип выводить. error: no viable constructor or deduction guide for deduction of template arguments of 'S' Что довольно логично. Но оказывается, есть возможность указать "хинт" для того, какой тип должен выводить конструктор класса! template<typename Iter> S(Iter, Iter) -> S<typename std::iterator_traits<Iter>::value_type>; И тогда вот это компилируется: https://godbolt.org/z/MaPo7PsP4 std::vector<double> v; auto s2 = S(v.begin(), v.end()); static_assert(std::same_as<decltype(s2), S<double>> ); У Константина дальше в докладе было всякое, какие у этого есть побочные эффекты. Посмотрите доклад потом сами. Много примеров есть прямо вот тут: https://eel.is/c%2B%2Bdraft/over.match.class.deduct А меня просто понесло. Мне так нравятся типы, которые меняются при каждом копировании, кто бы знал (и да, весь пост написан только ради этой ржаки): https://godbolt.org/z/4EMnjWsza #include <concepts> struct Charmander {}; struct Charmeleon {}; struct Charizard {}; template<typename Form> struct Pokemon { Pokemon(Form) {} template<typename OtherForm> Pokemon(const Pokemon<OtherForm>&) {} }; Pokemon(const Pokemon<Charmander>&) -> Pokemon<Charmeleon>; Pokemon(const Pokemon<Charmeleon>&) -> Pokemon<Charizard>; Pokemon(const Pokemon<Charizard>&) -> Pokemon<Charmander>; int main() { Pokemon p1 = Charmander{}; Pokemon p2 = p1; Pokemon p3 = p2; Pokemon p4 = p3; static_assert(std::same_as< decltype(p1), Pokemon<Charmander> >); static_assert(std::same_as< decltype(p2), Pokemon<Charmeleon> >); static_assert(std::same_as< decltype(p3), Pokemon<Charizard> >); static_assert(std::same_as< decltype(p4), Pokemon<Charmander> >); }

  • 3 авг.1 267 просмотров25 реакций6 пересылок

    Убедительная просьба - обновляйте компиляторы. Помимо внедрения множества способов выстрелить себе в ногу, новые версии компиляторов чинят разные веселые баги. Вот к примеру вот этот код: struct S { int x; auto foo() { return [*this](this auto&& self) { __builtin_printf("%d ", x); x = 10; }; } }; Возвращаемая лямбда возвращаемая из метода foo капчурит this, и потом модифицирует значение. Но что будет, если мы сделаем лямбду константной? int main() { S s{ 5 }; const auto l = s.foo(); l(); s.x = 15; l(); __builtin_printf("%d\n", s.x); } вот тут clang18.1 нормально сработает и модифицирует константный класс. clang19.1 выдаст ошибку компиляции. А gcc15.2 упадет с segfault https://godbolt.org/z/8qqbehqhE

  • 1 авг.1 192 просмотров33 реакций2 пересылок

    Я сначала подумал, что надо на такие мероприятия футболку брать с QR кодом канала на спине, пиарить канал на конфе. А потом подумал: меня же спалят как админа этого канала... Свят-свят-свят. P.S. новый пост зашедулен на понедельник :)

  • 10 авг.881 просмотров32 реакций8 пересылок

    Ладно, разумеется прошлый пост был набросом. Никогда не обновляйте компиляторы без очень серьезных на то причин. Мало ли что там сломается, а у вас не будет команды все это чинить. Вот к примеру. Вложенные лямбды с параметр паком. #include <utility> template <std::size_t N, std::size_t M> concept C = requires { []<std::size_t... Is>(std::index_sequence<Is...>) requires requires { []<std::size_t... Js>(std::index_sequence<Js...>) requires requires { true; } {}(std::make_index_sequence<M>{}); } {}(std::make_index_sequence<N>{}); }; static_assert(C<2, 3>); В clang 17 они нормально компилируются. В clang 20 они крашат компилятор. В clang 22 это починили и они компилируются. В текущем транке снова ломают компилятор. Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.

  • 10:26416 просмотров9 реакций2 пересылок

    Ладно, искать баги в компиляторах весело, но недостаточно. Давайте поиграемся в чуть более интересные игры. Вспомним, что у нас в С++ есть правило: структура не может иметь нулевой размер. Потому что у ее инстанса должен быть адрес, и здесь С++ как-то забыл про Zero Cost и все такое. Зато С++ вспомнил про Zero Cost, когда мы примешиваем пустую структуру в наследование другой. struct Empty1 {}; struct Empty2 {}; struct NonEmpty { int Value; }; struct MyClass1 :public NonEmpty ,public Empty1 ,public Empty2 { }; struct MyClass2 :public Empty1 ,public NonEmpty ,public Empty2 { }; struct MyClass3 :public Empty1 ,public Empty2 ,public NonEmpty { }; static_assert(sizeof(NonEmpty) + sizeof(Empty1) + sizeof(Empty2) == 6); static_assert(sizeof(int) == 4); static_assert(sizeof(MyClass1) == 4); static_assert(sizeof(MyClass2) == 4); static_assert(sizeof(MyClass3) == 4); int main() {} Вот эта штука скомпилируется. Тоесть у нас структуры без размера имеют размер, но этот размер не добавляется к размеру общей структуры. Всвязи с чем я упоролся, и написал вот такой класс: struct Empty { void zero() { std::memset(this, 0, sizeof(*this)); } }; Свиду ничем не примечательный класс, который просто заполняет себя нулями. И это нормально работает для непустого класса, но пустой класс такими выкрутасами ломает соседние поля в классе, который его отнаследовал. struct CompressedPair : Empty { CompressedPair(Empty e, bool x) : Empty(e), b(x) { } bool b; }; int main() { auto a = CompressedPair({}, true); __builtin_printf("%b\n", a.b); a.zero(); __builtin_printf("%b\n", a.b); return 0; } Выводит 1 0 (да, да, это UB, я знаю, прекратите. Мы тут поржать собрались или где?) Но где-то тут в безумии зараждается идея... У нас же есть no_unique_address. А на gcc он кладет данные в padding других структур. #include <iostream> template <typename T> struct optional { union { char foo; T val; }; [[no_unique_address]] bool engaged; }; template <typename View> struct reverse_view { [[no_unique_address]] optional<typename View::iterator> cache; View base_; }; struct MyView { using iterator = int*; char begin_; }; int main() { std::cout << "sizeof = " << sizeof(reverse_view<MyView>) << std::endl; // clang - 24, gcc - 16 std::cout << "alignof = " << alignof(reverse_view<MyView>) << std::endl; } И тут я очень хотел развернуться мыслью, но быстро нашел уже закрытый баг с std::expected. (половина примеров выше взята из обсуждения этого бага, потому что они нагляднее чем то, с чем работал я). Если коротко, то методы expected могли зачищать переменные вокруг себя, если те оказывались в из padding. Потому что emplace пересоздавал внутренний объект целиком. Но, увы, примера воспроизводящего это дело у меня нет. А после этого примера все остальное рядом меркнет. Увы.