C++: Хроники Дурки🚑
838 subscribers
4 photos
41 links
Очень люблю C++, но это скорее уже стокгольмский синдром.
Постоянно нахожу способы стрельнуть себе в ногу.
Download Telegram
Итак, у нас, согласно cppreference, c 11-ого стандарта есть набор целочисленных типов типа:


int8_t
int16_t
int32_t
int64_t

signed integer type with width of exactly 8, 16, 32 and 64 bits respectively
with no padding bits and using 2's complement for negative values
(provided if and only if the implementation directly supports the type)



Ну давайте поиграемся.

Что будет выведено вот на это:


std::cout << "int16_t: \n"
<< static_cast<int16_t>(00) << '\n'
<< static_cast<int16_t>(48) << '\n'
<< static_cast<int16_t>(65) << '\n'
<< std::endl;


Правильный ответ:

```
int16_t:
0
48
65
```


А вот на это?


std::cout << "int64_t: \n"
<< static_cast<int64_t>(00) << '\n'
<< static_cast<int64_t>(48) << '\n'
<< static_cast<int64_t>(65) << '\n'
<< std::endl;


Правильный ответ:

```
int64_t:
0
48
65
```

А вот на это?


std::cout << "int8_t: \n"
<< static_cast<int8_t>(00) << '\n'
<< static_cast<int8_t>(48) << '\n'
<< static_cast<int8_t>(65) << '\n'
<< std::endl;


Правильный ответ:

```
int8_t:
0
A
```


А все почему?

Потому что идите все нахер, int8_t - это char.


Особенно это приятно, когда у вас из логов пропадает что-то такое:



enum class Direction : int8_t {
LEFT, RIGHT, UP, DOWN
};

// ...



std::cout << static_cast<int8_t>(Direction::LEFT)
<< std::endl;

using Int = std::underlying_type_t<Direction>;
std::cout << static_cast<Int>(Direction::LEFT)
<< std::endl;




Ну вот и нахрен так жить?
😁22👍4🔥1💊1
Прекрасный и интуитивный auto.


Давайте возьмем вот такой код для старта.

int main() {
const int x = 42;
auto a = x;
auto& b = x;
}


Давайте попробуем угадать, какого типа будут переменные a и b?

decltype(a)
decltype(b)


Это прекрасное:

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

Тоесть при копии у нас теряется константность. А при получении ссылки не теряется.

И если разобраться.... Это абсолютно логично!!!
Но блин, когда ты только только глядишь - это сначала выбивает тебя немного в ступор.

А что делать, если мы хотим что-то менее логичное, но более интуитивное?

А что-то такое:

#include <iostream>
#include <type_traits>

int main() {
const int x = 42;
decltype(auto) a = x;
auto& b = x;
// ...
}
👍102
Маленька классика на этой неделе.

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


#include <iostream>

int main() {
int x = 42;
std::cout << sizeof(++x) << '\n';
std::cout << x << '\n';
}



Да, все верно:

```
4
42
```

Тут все просто: sizeof не вычисляет выражение. Вообще никак.

То есть ++x написан,
вы его видите,
компилятор его видит,
Бог его видит,
но реально инкремента не происходит.

Только clang немного поплюется warning-ами


Ну чтож... всего лишь еще один повод угодить в дурку.
🔥19😁103
Пример из того самого доклада.


Это просто прекрасное. Я где-то слышал утверждение, что лямбы имеют zero cost, что должны соптимизироваться во что-то такое же, как и исхордный код.

Ну так вот для такого кода:


int main() {
auto div = [](double a, double b) { return a / b; };
double a = 0.5;
double b = 0.01;

std::cout << (int)(a / b) << std::endl;
std::cout << (int)div(a, b) << std::endl;
}



У меня сработало в трех случаях из четырех. В четвертом вышло


49
50


Счастливого дебага с*****.
😱11🤯8😁21🥴1
Есть вот такая шляпа:


struct Buffer {
std::vector<int> data{1, 2, 3};

const std::vector<int>& Items() const {
return data;
}
};

Buffer MakeBuffer() {
return {};
}

int main() {
for (int x : MakeBuffer().Items()) {
std::cout << x << ' ';
}
std::cout << '\n';
}


Что будет выведено?

Если запускать gcc/clang с


-fsanitize=address -fsanitize=undefined


То они взворвуться всякими ошибками на доступ к памяти.


==1==ERROR: AddressSanitizer: stack-use-after-scope on address 0x6cc5bdef0060 at pc 0x5806274e084a bp 0x7ffc913eefb0 sp 0x7ffc913eefa8
READ of size 8 at 0x6cc5bdef0060 thread T0
#0 0x5806274e0849 in __gnu_cxx::__normal_iterator<int const*, std::vector<int, std::allocator<int>>>::__normal_iterator(int const* const&) /opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/16.0.1/../../../../include/c++/16.0.1/bits/stl_iterator.h:1059:20



Без них

clang выводит:

692359176 -2055096448 -2055096448


icc выводит:

163141 0 -143200117


gcc и msvc не выводит ничего....


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

А под капотом - так называемый "13-летний баг", историю которого можно отследить вот по этим ссылкам:
https://cplusplus.github.io/CWG/issues/900.html
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2012r2.pdf
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2644r1.pdf
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2718r0.html

Какие выводы мы можем сделать по итогу этих ссылок? Что в С++23 починили таки эту проблему.

Содержательно fix такой: в [class.temporary] добавили четвёртый специальный контекст, в котором временный объект живёт дольше обычного — если он создан в for-range-initializer range-based for, то его lifetime продлевается на жизнь скрытой ссылки, то есть фактически на весь цикл.


The fourth context is when a temporary object other than a function parameter object is created in the for-range-initializer of a range-based for statement. If such a temporary object would otherwise be destroyed at the end of the for-range-initializer full-expression, the object persists for the lifetime of the reference initialized by the for-range-initializer.


И оно поддержано уже в gcc/clang (но не других компиляторах).

Пример с теми же санитайзерами выводит:


1 2 3


Приятно, что всего через 13 лет один из самых болезненных багов из С++ ушел...
🔥24😭6👍21
Посмеемся?


#include <iostream>
#include <string_view>

struct Base {
void Set(std::string_view) { std::cout << "string\n"; }
void Set(int) { std::cout << "int\n"; }
};

struct Derived : Base {
void Set(bool) { std::cout << "bool\n"; }
};

int main() {
Derived d;
d.Set("hello");
}


Что выведет?

Ответ конечно `bool`:


Почему так?

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

И дальше d.Set("hello") уже ищет только среди перегрузок Derived.
А const char* в bool конвертируется просто замечательно.

И по нашей любимой традиции - ни одного ворнинга ни в одном из компиляторов.
🔥23😁921
Сижу на CppRussia.

Пока тут каждый второй слайд первого же доклада - кандидат на пост сюда....
😁54💯7🔥4
Не могу не поделиться самым веселым, на мой взгляд, примером из доклада великолепного Константина Владимирова. Он делал анонс доклада в своем 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🤯103🔥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(), они вот там, в базовом классе, рукой подать.

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

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


Особенно приятно при большом рефакторинге, когда меняешь не-шаблонный класс на шаблонный, а он потом в произвольных местах кода ломается...
26🍌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 года.

Пипец какая жесть.
😁24🤯12😐2