C++: Хроники Дурки🚑
908 subscribers
5 photos
50 links
Очень люблю C++, но это скорее уже стокгольмский синдром.
Постоянно нахожу способы стрельнуть себе в ногу.
Download Telegram
Не могу не поделиться самым веселым, на мой взгляд, примером из доклада великолепного Константина Владимирова. Он делал анонс доклада в своем tg канале.

Пример вот такой:


template <auto T = []{}>
struct S {};

S a; S b;


В чем тут цимес. У нас T - это лямбда. И если мы таким образом определяем переменные, то в тип S записываются разные лямбды, и у нас получаются два разных типа:


static_assert(
!std::is_same_v<decltype(a), decltype (b)>
);


Если же мы явно укажем пустые треугольные скобки вот так:


S<> a, b;

static_assert(
std::is_same_v<decltype(a), decltype (b)>
);


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


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


S a, b;


Давайте вы попробуете угадать?

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

clang падает с ошибкой

```
error: template arguments deduced as 'S<(lambda at <source>:4:20){}>' in declaration of 'a' and deduced as 'S<(lambda at <source>:4:20){}>' in declaration of 'b'
7 | S a, b;
```


А gcc считает, что это два разных типа.

```
static_assert(
!std::is_same_v<decltype(a), decltype (b)>
);
```

Пруф.


Вцелом доклад Константина был просто прекрасным, и я, наверное, понатырю сюда еще примеров из его доклада через пару месяцев. А когда он выйдет в открытый доступ - обязательно дам ссылку. Я был просто в восторге от дурки, которую он показывал.
👍18🤯113🔥2
Сегодняшняя рубрика называется "обычный шаблонный код, который компилируется только после жертвоприношения".

Что выведет вот этот код?

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(), они вот там, в базовом классе, рукой подать.

Но компилятор такой:

Нет.
В шаблонах я сначала притворяюсь, что базового класса почти не существует.


Особенно приятно при большом рефакторинге, когда меняешь не-шаблонный класс на шаблонный, а он потом в произвольных местах кода ломается...
29🍌1
Разбираем письма читателей.


Нам прислали вот такой вот код:



#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 все еще будет возвращаться ненеулевой код....
🤯295😱3
Вот сколько я дурки повидал (у меня канал про это целый заведен так-то !!!) но с удивлением я осознал, что вот такая штука


%:include <iostream>

int main() <%
int a<:3:> = <% 10, 20, 30 %>;
std::cout << a<:1:>;
%>


Компилируется во всех комипляторах...

Потому что язык заботливо сохранил диграфы — на случай, если ваша клавиатура из 1973 года.

Пипец какая жесть.
😁29🤯15😐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


Вот так вот.
19👍9
Сорян, я выяснил, что у меня давно не было публикаций... А у меня просто один пост пропал. Не могу его найти ни в отложках ни в опубликованных...

Такчто сейчас перетасую расписание и пошел искать.
🌚214🤝2
Повторяю пропавший пост.

Он был о докладе Евгения Ерохина с прошедшего 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 - самое время разочароваться в собственных знаниях :)
😭23👏51
Мой дорогой друг вот с этого канала о С++:

@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;


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

Думаю, в комментариях он подскажет, был ли этот пример найден в продуктовой задаче...
🤯17🔥9😁2🌚2🫡1
Ой, я когда-нибудь брошу этот сраный язык.

Давайте разберем по шагам.



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();
}


Я вот хочу куда-нибудь в шаблонный код загнать, пранкануть, таксказать, коллег.
🔥28🤯25😁123💊3🤡1
Вместо дурки я сегодня продолжаю пересматривать доклад великолепного Константина Владимирова.

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

Вместо дурки хочу поделиться интересной конструкцией, о которой до этого не знал, но почерпнул из того доклада.


Пусть у нас есть класс:


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>
>);
}
😁29👌42🤯2
Приятно видеть, что 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

Но меня уже разоблачили, плюсовик я не настоящий и гениальности очередного замысла могу не понять. Как хорошо, что у меня канал не образовательный а развлекательный 🙂
😁215🥴2🔥1🗿1
Те, кто со мной регулярно общаются, знают, что я ненавижу сраный 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.
🤷‍♂20😁11🤡3👎2😭2🔥1
Я сначала подумал, что надо на такие мероприятия футболку брать с QR кодом канала на спине, пиарить канал на конфе.

А потом подумал: меня же спалят как админа этого канала... Свят-свят-свят.

P.S. новый пост зашедулен на понедельник :)
😁27🤡4❤‍🔥2
Убедительная просьба - обновляйте компиляторы.

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

Вот к примеру вот этот код:


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
💊20🤔5
Ладно, разумеется прошлый пост был набросом.

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

Вот к примеру.

Вложенные лямбды с параметр паком.

#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 это починили и они компилируются.
В текущем транке снова ломают компилятор.


Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.
🤡13🦄7😁3🤔21🔥1💩1👾1