C++: Хроники Дурки🚑
960 subscribers
6 photos
57 links
Очень люблю C++, но это скорее уже стокгольмский синдром.
Постоянно нахожу способы стрельнуть себе в ногу.
Download Telegram
Я сначала подумал, что надо на такие мероприятия футболку брать с 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
💊21🤔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 это починили и они компилируются.
В текущем транке снова ломают компилятор.


Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.
🤡15🦄10😁3🤔2❤1🔥1💩1👾1
Ладно, искать баги в компиляторах весело, но недостаточно.

Давайте поиграемся в чуть более интересные игры.

Вспомним, что у нас в С++ есть правило: структура не может иметь нулевой размер. Потому что у ее инстанса должен быть адрес, и здесь С++ как-то забыл про 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 пересоздавал внутренний объект целиком.


Но, увы, примера воспроизводящего это дело у меня нет. А после этого примера все остальное рядом меркнет. Увы.
❤15🔥2💊1
#толькосвоимемы

Код взят тут.


if (dim == 0) idx_dim = i0;
else if (dim == 1) idx_dim = i1;
else if (dim == 2) idx_dim = i2;
else if (dim == 3) idx_dim = i3;

const struct htp_tensor * src = (idx_dim < src0->ne[dim]) ? src0 : src1;

uint32_t s0 = i0;
uint32_t s1 = i1;
uint32_t s2 = i2;
uint32_t s3 = i3;

if (dim == 0 && src == src1) s0 -= src0->ne[0];
if (dim == 1 && src == src1) s1 -= src0->ne[1];
if (dim == 2 && src == src1) s2 -= src0->ne[2];
if (dim == 3 && src == src1) s3 -= src0->ne[3];


Вообще по библиотеке много кода, который заставляет задумываться о вечном.
🔥14
Если кто не знаком с библиотекой nlohmann/json - она прекрасна. Мои мысли о том, как должна выглядеть библиотека для пользователя, она начала формировать еще году в 2012. (я гне нашел подтверждений, что она тогда уже была, но по ощущениям где-то тогда и появилась)

Но речь не об этом.

Как написано в cppreference, если ключи равны, то и хеши от них равны.

```
Given the following values:
h, an object of type std::hash<Key>.
k1 and k2, objects of type Key.
All following requirements are satisfied:
- If k1 == k2 is true, h(k1) == h(k2) is also true.
- Unless std::hash<Key> is a program-defined specialization, h(k1) will never throw an exception.
```

Иногда срабатывает даже если это объекты разных типов



const auto x1 = 42;
const auto x2 = 42u;

__builtin_printf("%b\n\n", x1 == x2);
__builtin_printf("%#08zx\n%#08zx\n",
std::hash<int>{}(x1),
std::hash<unsigned>{}(x2));


1

0x00002a
0x00002a


А вот если вы упакуете те же типы в обертку типа json:


const auto j1 = json{x1};
const auto j2 = json{x2};


__builtin_printf("%b\n\n", j1 == j2);
__builtin_printf("%#08zx\n%#08zx\n",
std::hash<json>{}(j1),
std::hash<json>{}(j2));


Тут уже как повезет. Конкретно для этой библиотеке не сраслось:

1

0x286fec1b50
0x286fec1b91


Потому что там есть своя специализация. И с ней это правило для hash не работает.


namespace std // NOLINT(cert-dcl58-cpp)
{

/// @brief hash value for JSON objects
/// @sa https://json.nlohmann.me/api/basic_json/std_hash/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
struct hash<nlohmann::NLOHMANN_BASIC_JSON_TPL> // NOLINT(cert-dcl58-cpp)
{
std::size_t operator()(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j) const
{
return nlohmann::detail::hash(j);
}
};


А потому эту штуку очень весело бывает использовать в hash-мапах, когда два одинаковых объекта могут стать разными ключами.

Сохраняем бдительность!!!!
🫡19🥴2❤1👍1
Всяко разное болезненное есть в С++, из всего болезненного одно из самых болезненных - это friend функции. Если жизнь и рассудок дороги вам - не объявляйте такие функции вообще никогда.

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

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

Пусть у нас есть вот такой класс:



class A {
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
void f(A& a, B& b){
b.y = 1;
a.x = 1;
}
};
};


Да, странненький и убогенький, но вообще весь мой код примерно такой.

Так вот в нем есть класс, у него есть вложенный класс, и в нем есть функция, которая модифицирует объекты и своего класса, и того, в который он вложен.


void f(A& a, B& b){
b.y = 1;
a.x = 1;
}


И как мы можем видеть из ссылки выше - все прекрасно работает, и проблем ни у кого не вызывает.


int main() {
auto a = A(1);
auto b = A::B(2);

b.f(a, b);

return 0;
}


Но давайте теперь сделаем эту функцию friend и вынесем за пределы класса.


class A {
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
friend void f(A& a, B& b);
};
};

void f(A& a, A::B& b){
b.y = 1;
a.x = 1;
}


Милорд, у вас родился кудяблик, и эта штука перестала компилироваться:


int main() {

auto a = A(1);
auto b = A::B(2);

f(a, b);

return 0;
}


Потому что что? Потому что правильно, мы дружим только с B, а вот с A не дружим.


<source>:15:7: error: 'x' is a private member of 'A'
15 | a.x = 1;
| ^
<source>:2:9: note: implicitly declared private here
2 | int x;
|


Мне тут поступал запрос на глубину моих постов, поэтому я потратил целых 10 минут на изучение вопроса прежде чем пошел строчить гневный пост в интернеты, и нашел вот такое:


https://cplusplus.github.io/CWG/issues/3193.html

тут что-то про inhabiting the class scope, и насколько я понял, вот такая ошибка компиляции - это самовольство компиляторов, а в стандарте про это вроде как ничего внятного сказано не было... Или было, но я, в скудоумии своем, не разобрался...

Кароче, friend - это одна их тех вещей, которых я стараюсь избегать в своем коде... Рано или поздно я начну избегать всего С++
❤17😁10👍2
Мошенники заставили пенсионерку из Москвы переписать квартиру на Rust
😁88🤡6❤1💯1
Я в очередной раз искал медь, а нашел золото.

Я чуть-чуть погрузился в bit_cast. Ввели такой в С++20 каст побитовый.

Он передает бит к биту из одного объекта в другой.

Условия просты:

```
sizeof(To) == sizeof(From) is true;
is_trivially_copyable_v<To> is true; and
is_trivially_copyable_v<From> is true.
```

И я искал воспроизводящий пример как он ведет себя с padding byte и ломается ли у нас что-то (если у вас такой есть - несите в предложку).

Но наткнулся на вот такой веселый пример.

struct alignas(int) Empty {};
static_assert(sizeof(Empty) == sizeof(int));

int main() {
const int x = std::bit_cast<int>(Empty{});
std::printf("%d %d", x == 0, x != 0);
}


И у нас clang весело выводит

0 0


x и равен нулю, и неравен нулю (оба сравнения false).

И если вы думаете, что на этом все закончено, то нет. Если убрать флаг -O2 из флагов компиляции, вывод будет

0 1


А в gcc вывод с O2 и без O2 выглядят так

1 0

0 1


Кароче, bit_cast временно вычеркиваю из инструментов до дальнейшего разбирательства.
😁35🤯13🔥2❤1👍1🤔1
Ковырял тут вот эту библиотеку:

https://github.com/yandex/yaff/blob/c815bc0a2469201542c4734257628a98ed794c30/include/yaff/base.h#L146-L160

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

Нашел там вот такой код:


template <typename T>
inline constexpr T XorDef(T v, T d) noexcept {
return v ^ d;
}

template <>
inline constexpr float XorDef(float v, float d) noexcept {
return std::bit_cast<float>(std::bit_cast<uint32_t>(v) ^ std::bit_cast<uint32_t>(d));
}

template <>
inline constexpr double XorDef(double v, double d) noexcept {
return std::bit_cast<double>(std::bit_cast<uint64_t>(v) ^ std::bit_cast<uint64_t>(d));
}


И пытался понять, зачем он там.

Что выяснилось. Мы хотим в сообщении "дефолтное" значение записывать нулевыми байтами. При этом при чтении хотим получать из нулевых байтов дефолтное значение. Поэтому мы при чтении делаем вот так:


YAFF_PURE T ReadValueUnsafe(const FieldOffset offset, const T defaultVal) const noexcept {
return XorDef(yaff::ReadValue<T>(Message() + offset), defaultVal);
}
YAFF_PURE T ReadValueUnsafe(const FieldOffset offset, const T defaultVal) const noexcept {
return XorDef(yaff::ReadValue<T>(Fields() + offset), defaultVal);
}


А при записи вот так:


void AddField(FieldId id, T value, T def) {
WriteField<T>(id, XorDef(value, def));
}



Из приятного: если наше дефолтное значение - нули, то компилятор этот xor соптимизирует.

Это ли не блеск, это ли не шик, это ли не прелесть.
❤16🥴3🔥2🤯2💅2
Делюсь бесполезными знаниями.

На x86 код


counter.fetch_add(1, std::memory_order_relaxed);


И


counter.fetch_add(1);


Выполняются одинакого быстро. Это бесполезное знание.

Полезное знание тут заключается в том, что, оказывается, бенчмарки можно запускать в godbolt. (Бенчмарк не мой, подглядел в другом месте). (А ещё на самом деле в более сложной функции relaxed допускает переупорядочивание недоступное при seq но это вообще сложные материи и не сумел сконструировать пример, который бы это воспроизводил).

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

Ну вот теперь мы это знаем. Что же это нам дает? Да ничего не дает.

Если мы походим по популярным проектам, то увидим, что везде fetch_add используется с relaxed.

PyTorch
Folly
LevelDB
Boost
gRPC

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

Но пусть я делаю что-то только для x86. Допустим я разобрался, и умею читать всякое такое (а я не умею, я - деградарий):


define void @inc1(ptr nocapture %p) nounwind ssp {
; CHECK-LABEL: inc1:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: lock incl (%rdi)
; CHECK-NEXT: retq
;
; SLOW_INC-LABEL: inc1:
; SLOW_INC: # %bb.0: # %entry
; SLOW_INC-NEXT: lock addl $1, (%rdi)
; SLOW_INC-NEXT: retq
entry:
%0 = atomicrmw add ptr %p, i32 1 monotonic
ret void
}


Есть ли у меня мотивация писать



counter.fetch_add(1);

???

Никакой мотивации. Я напишу relaxed просто на всякий случай. А мало ли что.

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

И как бы мне получать полезные знания, не тратя время на бесполезные?

А тут ещё эти ваши нейронки, дискредетирующие знания в голове вцелом... Нипанятна...
💊19❤4😁4🤔2👍1