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

Мы заводим структуру данных, которая конструируется от аргумента типа double. И вторую структуру, которая конструируется от первой.

А дальше создадим переменную второго типа.

#include<iostream>

struct A {
double v;
explicit A(double d) : v(d) {}
};

struct B {
double v;
explicit B(A a) : v(a.v) {}
};

int main(){
double d = 3.14;
B b(A(d));
}


Проигнорируем ворнинги компиляторов (кого оно волнует, компилируется же).

А что будет, если мы выведем значение перменной b?

int main(){
double d = 3.14;
B b(A(d));

std::cout << b << std::endl;
}


Внезапно, выведет 1:


Program returned: 0
1


Чта?!

Ну, пришло время почитать ворнинги. И выясняем, что вот это:
    B b(A(d));


не переменная. Это - функция. Дело в том, что вот два таких объявления функций, в целом, одинаковые:

void f(int a);
void q(int(a));


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

И.... Я не знаю, как оно так в итоге так получается, но стреляет время от времени...

Добро пожаловать к нам в дурку.
🤯115
Приколько поговорить про гадость, которую можно отловить на этапе компиляции.

Но есть разная дурка, которая аффектит рантайм, но проходит мимо всех ворнингов (иногда это ловят всякие clang-tidy и прочие PVS студии).

Вот канонический пример:


#include<iostream>
#include <map>
#include <vector>
#include <string>

using namespace std;
using Vec = vector<string>;

void f(pair<string, Vec>&& arg) {
for (const auto& v: arg.second) {
puts(v.c_str());
}
}

int main(){
auto m = map<string, Vec>{
{"first", {"1","2"}},
{"second", {"3","4","5"}},
};

for (auto&& p: m) {
f(std::move(p));
}
}


Мы создаем мапу (она будет создана в компайл тайме), а потом все объекты из нее эффективно муваются.

Но если мы внимательно посмотрим в исхродный код, то найдем там вот такое:

        call    memcpy@PLT


Я прошу прощения...? Какое копирование?! Где?

И вообще весь блок выглядит подозрительно похоже на конструктор копирования:

.LBB1_84:
mov qword ptr [rsp + 56], r13
mov r14, qword ptr [rbx + 32]
mov rbp, qword ptr [rbx + 40]
cmp rbp, 16
jb .LBB1_87
lea r15, [rbp + 1]
mov rdi, r15
call operator new(unsigned long)@PLT
mov qword ptr [rsp + 56], rax
mov qword ptr [rsp + 72], rbp
jmp .LBB1_89
.LBB1_87:
test rbp, rbp
je .LBB1_106
lea r15, [rbp + 1]
mov rax, r13
.LBB1_89:
mov rdi, rax
mov rsi, r14
mov rdx, r15
call memcpy@PLT
.LBB1_90:
mov qword ptr [rsp + 64], rbp
mov r14, qword ptr [rbx + 64]
mov qword ptr [rsp + 88], r14
movups xmm0, xmmword ptr [rbx + 72]
mov r15, qword ptr [rbx + 72]
movups xmmword ptr [rsp + 96], xmm0
xorps xmm0, xmm0
movups xmmword ptr [rbx + 64], xmm0
mov qword ptr [rbx + 80], 0
cmp r14, r15
je .LBB1_98


Не буду тут показывать, но, если провести перфоманс тесты, мы тоже увидим, что тут все тормозит.
И если детально разобраться: так и есть - это конструктор копирования.

Проблема вот в этой функции:

void f(pair<string, Vec>&& arg) 


Если поменять объявление этой функции на

void f(pair<const string, Vec>&& arg) {


то этот блок строк на 70 машинного кода уйдет, а перфоманс выровняется.

Почему так? Если посмотреть на cpp reference, то мы увидим, что

key_type Key
mapped_type T
value_type std::pair<const Key, T>


значение в мапе содержит константный ключ. А конвертировать из структуры с константным ключем в структуру с неконстантным ключом.... Это копирование!...


И ни одного ворнинга! Мой же ты любимый С++.
👍11😐5🔥3🗿2👌1
Внимание, шутка!!

Умножение в С++ некоммутативно.

Вот пример:


#include <iostream>

int main() {
std::cout << (2,0 * 2,5) << std::endl; // 5
std::cout << (2,5 * 2,0) << std::endl; // ???
return 0;
}


Выведет 5 только на первую строчку. А на вторую нет.
Живите с этим.


Разумеется, тут проблема в том, что в С++ числа с плавающей запятой пишутся с точкой. Запятая - это отдельный
оператор С++, который выводит второе значение.

Молодцы, что разгадали этот простенький паззл 🎁

К слову,
clang выводит ворнинг на это. А gcc только при включенном Wall.
😁87🌚5🤓2
Немного про новые стандарты: когда я показываю какую-то упячку, часто мне говорят "просто не пользуйся вот этим...".
Например, "не пользуйся new/delete, пользуйся умными указателями".

Или "не пользуйся конструкторами с круглыми скобками, пользуйся {}. И порядок зафиксирует и от кучи проблем избавит.".


Увы, новые конструкции просто создают другое подмножество проблем.

Вот к примеру, что выведет вот такой код?


#include <iostream>

int main() {
auto a = std::string{48};

std::cout << a << std::endl;
}



Правильно, он выведет 0. Потому что идет неявное преобразование инта к чару. И chr(48) == '0'.

Что характерно, вот такая строчка


auto b = std::string(48); // error: no matching function for call to


Не скомпилируется. Потому что у строки нет конструктора от char. Точнее есть, но там надо развлекаться веселее.


#include <iostream>

int main() {
auto b = std::string(48, 48.0);
std::cout << b << std::endl;
}


Это будет


000000000000000000000000000000000000000000000000


Так или иначе, но у нас новые конструкции просто создают новые проблемы.


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

Человек, который прислал мне похожий пример, утверждает, что он пришел из продакшн кода.


#include <iostream>
#include <unordered_map>

int main() {
std::unordered_map<std::string, std::string> m;
double i = 65.5;
m["hello"] = i;

std::cout << m.at("hello");
return 0;
}


Что тут происходит? double кастится к int, int кастится к char, char кастится к string, в мапу записывается буква A. Прекрасно!
🥴14🔥3😢32🤩1
Ну а как еще можно было назвать канал, как не "Дурка"?

Ну сам факт, что вот такой код компилируется, запускается, а еще и не падает, и выводит 0!!!



#include <iostream>

class Test {
public:
void test() {
std::cout << this << std::endl;
}
};

Test & create()
{
return *((Test*)NULL);
}

int main()
{
Test &t = create();
t.test();
}


Нет, конечно, понятно даже почему, если разобрать поднаготную устройства функций-членов класса. Но блииииин........
💊9😨31👍1👎1😢1
Еще одна абсолютно бесполезная, но унаследнованная штука в С++. Заголовок может заинклюдить сам себя.


Например вот такой заголовок:


// megaheader.hpp
#ifndef MEGAHEADER_HPP
int foo() {
return 1;
}
#define MEGAHEADER_HPP
#include "megaheader.hpp"

#else
int bar() {
return 2;
}

#endif


Сработает совершенно нормально.


#include <iostream>

#include "megaheader.hpp"

int main() {
std::cout << "foo: " << foo() << std::endl;
std::cout << "bar: " << bar() << std::endl;
return 0;
}




foo: 1
bar: 2


Ни ворнингов, ничего. А самое главное - как бы так передефайнить разные куски, чтобы на этом механизме устроить перебор? Ну, перебрать все комбинации из 5 дефайнов, и для каждого определить функцию? Было бы... Забавно?...
🤔4😁2🥴21👍1
Причины, по которым этот канал называется "дурка".

Вот такой вот метод:


void Suicide() {
delete this;
}


Компиляторы сожрут. И не выведут ни одного ворнинга при Wall.

Вот полный пример.


#include <iostream>
#include <vector>

struct S {
int a = 13;
std::vector<int> v{};
void Suicide() {
delete this;
}

};

int main(){
auto* s = new S();
s->Suicide();
return 0;
}


У меня все, увидимся в дурдоме
🤡10🤝5💊3🤷‍♂21
Ну, к разного рода наркомании, что


std::cout << a[42] << std::endl;
std::cout << 42[a] << std::endl;


Это одно и то же - все уже привыкли.

А как насчет инициализации массива с указанием ренжа индексов?
Типа:


static constexpr int a[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };


Или инициализации единицы для вайтспейстов:


int whitespace[256] =
{
[' '] = 1, ['\t'] = 1,
['\f'] = 1, ['\n'] = 1,
['\r'] = 1
};


Это нормально без дополнительных выкрутасов работает на clang, правда, грустит и плюется ворнингами.

Эта штука называется Designated Initializers, и является частью ISO C99, а С++ пытается быть совместимым с С90, а 99 - оно так, по желанию.

На годболте у меня так и не получилось скомпилировать вот такой пример на gcc:


#include <iostream>

int main() {
static constexpr int a[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };

std::cout << a[42] << std::endl;
std::cout << 42[a] << std::endl;

int whitespace[256] =
{
[' '] = 1, ['\t'] = 1,
['\f'] = 1, ['\n'] = 1,
['\r'] = 1
};

return 0;
}



однако локально он нормально компилируется вот такой командной строкой:


g++ --std=gnu++23 main.cpp


Вот какие-то такие развлечения....
👏11😁9🥴61
Есть задачка, абсолютно практическая на самом деле.

Все знают про padding:

#include <iostream>

struct Foo {
char a; // 1
int b; // 4
double c; // 8
};

struct Bar {
char a; // 1
double c; // 8
int b; // 4
};

int main() {
std::cout << sizeof(Foo) << '\n'; // 16
std::cout << sizeof(Bar) << '\n'; // 24
}


Размер структуры Foo 16, а у Bar, несмотря на то, что переменные тех же типов, размер 24. Потому что double размера 8 должен быть помещен по адресу памяти, кратному размеру самой переменной, поэтому место между переменными заполняется 8 фейковыми байтами.


Это классика, но вопрос не про это. Утверждение:

Размер структуры невозможно поменять, переставив переменные в ней В ОБРАТНОМ ПОРЯДКЕ


Задача - доказать что утверждение верно или привести контрпример.

Как я сказал, задача на самом деле практическая, подробности в следующем посте.
Если вы очень любите питон, у меня для вас выход:


#include <iostream>
#include <stdio.h>
#include <fcntl.h>
#include <string>
#define print(data) cout<<data<<endl;
#define ord(data) int(data[0])
#define str(data) char(data)
#define open fopen
#define write(f, data) fputc(data, f)
using namespace std;
string input()
{
string s;
cin>>s;
return s;
};
FILE* f;
int chr;

int main() {
print("Enter:");
f = open("code.txt", "w");
chr = ord(input());
print(chr);
write(f, str(chr));
return 0;
}

P.S. тот, кто мне скинул этот код, ссылался на реальную лабораторную студента...

🤡🤡🤡🤡🤡🤡🤡🤡
💊18😍4🏆4👍2🔥2😐1🙈1
А вот в C23 можно тип объявлять прямо в return типе.


#include <stdio.h>

struct{int a; float b;} test()
{
return (typeof(test())){1337, 666.666};
}

int main()
{
auto a = test();
printf("%d %f\n", a.a, a.b);
return 0;
}


А вот в С++ такого нельзя:


error: new types may not be defined in a return type


Товарищи из комитета, отстаете. Где эти безусловно нужные всем языковые фичи?
💊17🤣6🫡5🔥1
Сегодня у нас поиски глубинного смысла в С++ на основе примеров, которые подсказывают подписчики.

В чем цимес. Лично мне в С++ не всегда понятно, что должно быть "нормальным поведением по-умолчанию", а что "нужно прописать явно".

Давайте посмотрим вот сюда:


#include <iostream>

struct Good {
int i;
bool operator==(const Good&) const = default;
};

int main() {

Good good1{1}, good2{2};
std::cout << (good1 == good2) << std::endl;

return 0;
}


Мы создали структуру, прописали, что ее можно сравнивать (по дефолту), и сравнили.

А теперь давайте унаследуем такую же структуру от пустой структуры



#include <iostream>

struct Empty {
// bool operator==(const Empty&) const = default;
};
struct Bad : Empty {
int i;
bool operator==(const Bad&) const = default;
/* error:
constexpr bool Bad::operator==(const Bad&) const'
is implicitly deleted because the default
definition would be ill-formed
*/
};

static_assert(sizeof(Good) == sizeof(Bad));

int main() {

Bad bad1{{}, 1}, bad2{{}, 2};
std::cout << (bad1 == bad2) << std::endl;

return 0;
}


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

Другими словами, нам надо явно писать что-то такое:


struct Empty {
bool operator==(const Empty&) const {
return true;
}
};


И вот не понятно, толи все логично, и я придераюсь. Толи правда неплохо бы генерировать операторы сравнения для пустых структур, а явно прописовать требовать только когда мы хотим их явно запретить. Я не знаю, я не понимаю...
🤔7😁32
И снова спасибо подписчикам за отборный контент.

Оказывается, в С++ можно объявить оператор каста к... void.



struct X {
// warning: Conversion function converting 'X' to 'void' will never be used
operator void() { std::cerr << "void\n"; }
};



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

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


#include <iostream>

struct X {
// warning: Conversion function converting 'X' to 'void' will never be used
operator void() { std::cerr << "void\n"; }
};

int main(int argc, char *argv[]) {
X x;

(void)x; // no
static_cast<void>(x); // no
x.operator void(); // YES!!!

return 0;
}


И вот это уже вообще взрыв мозга! 🤯
😁21💊11🔥1
Сегодня разбираем вот такой рабочий пример. (Рабочий в том смысле, что найден на работе).

Берем вот такой код:


// hpp

#include <unordered_map>

struct Foo {
int x;
int y;
};

struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};

// cpp

#include <string>

Foo& Bar::ForY(const std::string& v) {
return xx[v];
}

int main() {}


Внимательно на него смотрим. Потом смотрим еще внимательнее.
Не видим проблемы.

Смотрим еще раз, и опять не видим проблемы.

А потом отправляем его на компиляцию, и получаем ошибку:


/cefs/aa/aad5f6fdba80b622f643f9a5_clang-trunk-20260313/bin/../include/c++/v1/unordered_map:657:74: error: type 'const std::hash<std::string>' does not provide a call operator
657 | _LIBCPP_HIDE_FROM_ABI size_t operator()(const _Cp& __x) const { return __hash_(__x.first); }


У нас нет инстанса хеша для строки.

😣😣😣😣😣😣


Как это исправляется? Правильно, заголовок строки надо обязательно ставить выше заголовка мапы:


// hpp

#include <string>
#include <unordered_map>

struct Foo {
int x;
int y;
};

struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};

// cpp


Foo& Bar::ForY(const std::string& v) {
return xx[v];
}

int main() {}


Точнее на самом деле, разумеется, важен не порядок заголовков (хотя там отдельный геморой. И в большинстве codestyle-ах порядок заголовков указывается, хотя на моей памяти "правильный" порядок поменялся ровно на противоположный).

Нужно чтобы string была указана до объявления мапы.

Скомпилируется:

#include <string>
struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};


Не скомпилируется:


struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};
#include <string>


Скомпилируется:

struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};
#include <string>

// Foo& Bar::ForY(const std::string& v) {
// return xx[v];
// }


Скомпилируется:



struct Bar{
std::unordered_map<std::string, Foo> xx;

Foo& ForY(const std::string& v);
};

#include <string>

Foo& Bar::ForY(const std::string& ) {
return xx.begin()->second;
}




И это какая-то лютая хрень, которую не найти не исправить.

Как вообще оно так вышло?
🔥93
Некоторые вещи о С++ я знаю натурально против своей воли.

Давайте возьмем вот такие флаги компиляции


--std=c++2c -O2 -pedantic -Wall -Wextra -fsanitize=address -fsanitize=undefined


Для icc сделаем -std=c++20, потому что 2c он не знает.

Стартовый пример, который хотел показать.

Ни одного ворнинга на компиляторах не выдает.

Что в этом примере:


void foo() { // never called
if constexpr(false) { // never true
if (false) { // never true
constexpr auto call = [](auto arg) {
std::printf("called %d", arg);
};
void(B<A<tag>, decltype(call)>{});
}
}
}


Самая обычная функция, которая нигде в коде не вызывается. Внутри функции if constexpr (false) который какбы тоже не должен никогда вызваться. Я бы вообще ожидал что блок внутри выкенется из компиляции. Внутри этого блока if (false). Внутри котого лямбда (причем с аргументом и локальной переменной).

Внимание, вопрос! А можно ли как-то вызвать эту лямбду?

Оказывается да, и нам в этом помогает вот такая строчка:


void(B<A<tag>, decltype(call)>{});


Что за классы такие А и В? А вот как они объявляются:


class tag;

template<class>
struct A {
template<class>
friend constexpr auto get(A);
};

template<class K, class V>
struct B {
template<class>
friend constexpr auto get(K) { return V{}; }
};

И вуаля, теперь мы можем вызвать эту функцию вот таким кодом:


int main() {
get<tag>(A<tag>{})(42);
}


Много раз повторив добьемся того же эффекта. И ни одного ворнинга.


Да, объявив лямбду вот так:


constexpr auto call = [&](auto arg) {
std::printf("called %d", arg);
};


Получим веселую ошибку компиляции:


note: a lambda closure type has a deleted default constructor


(Да, я просто добавил `[&]`).


И я изначально, встретив нечно похожее, шел по пути усложнения кода. Потому что хотел избавиться от всех ворнингов, а, например, закомментрируем template в объявлении функции get, и хотябы gcc начнет сыпать хоть какими-то ворнингами:


template<class>
struct A {
// template<class>
friend constexpr auto get(A);
};

template<class K, class V>
struct B {
// template<class>
friend constexpr auto get(K) { return V{}; }
};



warning: friend declaration 'constexpr auto get(A< <template-parameter-1-1> >)' declares a non-template function


А icc так вообще перестанет собирать код:


internal error: assertion failed at: "func_def.c", line 1915 in scan_function_body

get(A<tag>{})(42);


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


#include <cstdio>

struct A {
friend auto get(A);
};

template<class V>
struct B {
friend auto get(A) { return V{}; }
};

void foo() { // never called
if constexpr(false) { // never true
if (false) { // never true
constexpr auto call = [](auto arg) {
std::printf("called %d", arg);
};
void(B<decltype(call)>{});
}
}
}

int main() {
get(A{})(42);
}


тоже прекрасно работает.

Вот ей богу, я эту грязь знать не хотел. 🤢
🤯164🔥2👍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
Посмеемся?


#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😁1021
Сегодняшняя рубрика называется "обычный шаблонный код, который компилируется только после жертвоприношения".

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

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

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

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


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