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

template<typename U>
struct Scheduler {

// ...

void Add(U&& callback) {
callback_ = std::move(callback);
}

// ...
U callback_;
};


И это какая-то иллюстрация Эффекта Манделлы, потому что часто на вопрос "что не так?" слышу простое, легкое для понимания, неправильное решение в духе

forwarding reference passed to std::move(), which may unexpectedly cause lvalues to be moved; use std::forward() instead


На самом деле, использование std::forward тут нежелательно.

Давайте разберем сразу 4 примера:

// 1
template<typename U>
struct Scheduler {
// ...

void Add(U&& callback) {
callback_ = std::move(callback);
}

// ...
U callback_;
};


// 2
template<typename U>
struct Scheduler {
// ...

void Add(U&& callback) {
callback_ = std::forward<U>(callback);
}

// ...
U callback_;
};


// 3
struct AnotherScheduler {
// ...

template<typename U>
void Add(U&& callback) {
callback_ = std::move(callback);
}

// ...
std::function<void()> callback_;
};


// 4
struct AnotherScheduler {
// ...

template<typename U>
void Add(U&& callback) {
callback_ = std::forward<U>(callback);
}

// ...
std::function<void()> callback_;
};


В первом примере все хорошо: тип зафиксирован в момент объявления инстанса класса
    Scheduler<std::function<void()>> sh;


поэтому в объявлении функции U&& -> это rvalue reference. И поскольку rvalue reference is lvalue, мы должны его мувать. Если использовать std::forward (Как в примере номер 2), то ничего страшного не случиться, но будет выглядеть странно, и заставит ругаться clang-tidy. Возможно, будут какие-то гадости, которые я не смог воспроизвести.


В примерах 3-4 все наоборот. Тип U определяется на этапе вызова функции, и его мувать опасно: у нас аргумент функции - forwarding reference, а значит там может быть как rvalue, так и просто ссылка. Если вы муваете ссылку внутри функции, тот, кто ее вызывает, может внезапно обнаружить, что переданный объект, который он не мувал, изчез:

    AnotherScheduler ash;
auto af = []() -> void {};
ash.Add(std::ref(af)); // moved here inside function
af(); // UB


А потому пример 3 - неверный и потенциальный источник багов. А пример 4 - норм.

Итого, "корректные" решения - 1 и 4. И, как и положено в С++, примеры 2 и 3 нормально скомпилируются... И даже будут как-то работать, наверное, в большинстве случаев, но делать так не надо.


И вообще, читаем cpp core guidelines.
👍16🔥3
Ну, к разного рода наркомании, что


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
Что бы вы сказали, найдя в коде вот такой кусок?


if(0){}else


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


if (0) {}
#if GCC
else if (gcc_comparison())
{
gcc_action();
}
#endif
#if CLANG
else if (clang_comparison())
{
claing_action();
}
#endif


И какую бы мы переменную не поставили - не будет ошибки компиляции из-за else перед if.

Но вот вам реализация foreach в QT для компиляторов от майкрософта (когда-то была, в последних версиях, очевидно, другая, и вообще не рекомендуется к использованию с версии Qt5.7).

И такую же реализацию можно найти в других проектах, наример, MAP_FOREACH в DynaMind Toolbox.


Note: Since Qt 5.7, the use of this macro is discouraged. Use C++11 range-based for, possibly with std::as_const(), as needed.





#define Q_FOREACH(variable, container) \

if (0) else
for (const QForeachContainerBase &_container_ = qForeachContainerNew(container); \
qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->condition(); \
++qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i) \
for (variable = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i; \
qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk; \
--qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk)



На кой черт оно тут?

А это следствие старого бага в VS 6: там внутри for цикла переменные не ограничивались в области видимости. Вот какой-то такой код нормально бы скомпилировался.


int main() {
for (int i = 0; i < 3; ++i) {
std::cout << i << std::endl;
}
std::cout << i << std::endl;
return 0;
}


А почему не сделать тогда if(true) ? А потому что тогда бы сработало


Q_FOREACH(a, b)
else {
// ...
}


Вот такую хрень в допотопные времена приходилось добавлять в код.

Как славно, что те времена давно прошли, и у нас на повестке новая дурка.
А вы в комментариях пишите (ну или загуглите), зачем нужен в этом макросе тернарный оператор:


qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->condition();
🥴5🔥3
#толькосвоимемы

Простите меня, христа ради, за этот мем...
😁326🙉6😭2😐1🙈1
Есть задачка, абсолютно практическая на самом деле.

Все знают про 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 фейковыми байтами.


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

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


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

Как я сказал, задача на самом деле практическая, подробности в следующем посте.
Сегодняшнее интересное меганаблюдение посвящено выбору стандартной библиотеки.

К примеру, мы можем скомпилировать программу с помощью clang двумя командами:


clang++ -O2 -std=c++20 -stdlib=libstdc++
clang++ -O2 -std=c++20 -stdlib=libc++

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

Например, вот такой код


struct A {
int i;
bool operator<(const A& other) const {
return i < other.i;
}
};

struct B {
int i;
bool operator<(const B& other) const {
return i < other.i;
}
};

int main() {

const auto first = std::make_tuple(A{1}, B{4});
const auto second = std::make_tuple(A{2}, B{3});

std::cout << ((first < second) ? "less" : "more") << std::endl;
std::cout << ((std::get<0>(first) < std::get<0>(second)) ? "less" : "more") << std::endl;


В обоих случаях выведет одинаковое:


less


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


const int* const pf = reinterpret_cast<const int* const> (&first);
const int* const ps = reinterpret_cast<const int* const> (&second);

std::cout << pf[0] << ' ' << pf[1] << std::endl;
std::cout << ps[0] << ' ' << ps[1] << std::endl;

std::cout << (pf[0] < ps[0] ? "less" : "more") << std::endl;


То увидим внезапное

На libstdc++


3 2
more


На libc++


2 3
less


Тоесть libstdc++ хранит реальные значения в обратном порядке... Хотя get<0> вычисляет так, как мы того и ожидаем. И это может иметь всякие неожиданные последствия для, например, производительности.

Однако, см предыдущий пост, это как минимум не должно сказаться на размере структур данных. Потому что разворотом порядка членов padding не меняется.

Но если вы найдете пример, где все-таки меняется - пишите.
1
Тут в комментах к этом посту прислали восхитительный пример. Хорошо я сидел, когда это увидел.

Вот такое:


struct Empty {};

struct Good : Empty {
int i;
char c;
};

struct Bad {
int i;
char c;
};

template <typename T>
struct S : T {
char c;
};

static_assert(sizeof(S<Good>) < sizeof(S<Bad>));

int main() {
return 0;
}


Админ канала ушел плакать в уголок и просить книжечку по GoLang. 😭😭😭😭
🔥10🥰3
Обычно когда спрашивают, что такое nullptr, получают в ответ, что это указатель равный нулю.

Более продвинутые говорят, что это зависит от имплементации, и приводят в пример какие-то embedded сценарии архитектур, до которых среднему программисту дела нет. Поэтому первая часть людей продолжает говорить, что в практическом поле nullptr - это ноль.

Но есть красивый пример, иллюстрирующий что это не так.


Природа заблуждения растет, как мне кажется, из проверки подстановкой указателя в if.

struct S {
char a;
char b;
int c;
int d;
};

int main()
{
auto s = S{'1', '2', 3, 4};

S* pS = nullptr;

if (!pS) {
std::cout << "pS" << std::endl;
std::cout << std::hex << "ptr: "<< *(int*)(&pS) << std::endl;
}

pS = &s;

if (pS) {
std::cout << "pS" << std::endl;
std::cout << std::hex << "ptr: "<< *(int*)(&pS) << std::endl;
}
}


Ну тут все очевидно. Выведет вот это:

pS
ptr: 0
pS
ptr: 7ede1034


Указатель нулевой, и логично, что проверка if (var) - это проверка на ноль, как мы и привыкли.
Но помимо указателя на объект в С++ есть еще указатель на члены класса.

    char S::* pSm = nullptr;

pSm = &S::a;

if (pSm) {
std::cout << "pS.a" << std::endl;
std::cout << std::dec << "value: " << s.*pSm << std::endl;
std::cout << std::hex << "ptr: "<< *(int*)(&pSm) << std::endl;
}

pSm = &S::b;


if (pSm) {
std::cout << "pS.b" << std::endl;
std::cout << std::dec << "value: " << s.*pSm << std::endl;
std::cout << std::hex << "ptr: "<< *(int*)(&pSm) << std::endl;
}


Что здесь важно. Указатель на член класса - это не адрес в памяти. Это смещение относительно адреса объекта к адресу члена. Таким образом, указатель на первый член класса всегда будет 0.

pS.a
value: 1
ptr: 0
pS.b
value: 2
ptr: 1


Еще более занятно, что прошла проверка if (pSm) для нуля. Потому что это валидное и даже ожидаемое значение, а в бинарном коде эта проверка выглядит так:

cmp     rax, -1


Ну и тут можно догадаться, но мы выведем кодом ниже и чему равен nullptr для членов класса:

    pSm = nullptr;

if (!pSm) {
std::cout << "pS.nullptr" << std::endl;
std::cout << std::dec << "value: " << s.*pSm << std::endl;
std::cout << std::hex << "ptr: "<< *(int*)(&pSm) << std::endl;
}

if (pSm) {
std::cout << "Unreacheable" << std::endl;
}



pS.nullptr
value:
ptr: ffffffff


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

Весело в С++ однако.....
🔥20👍1🤔1😢1
Если вы очень любите питон, у меня для вас выход:


#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
Пример честно украденный отсюда.

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

Пример выглядит следующим образом:


struct A {
using T = T1;
using U = U1;
operator U1 T1::*();
operator U1 T2::*();
operator U2 T1::*();
operator U2 T2::*();
};

inline auto which(U1 T1::*) { return "gcc"; }
inline auto which(U1 T2::*) { return "icc"; }
inline auto which(U2 T1::*) { return "msvc"; }
inline auto which(U2 T2::*) { return "clang"; }

int main() {
A a;
using T = T2;
using U = U2;
puts(which(a.operator U T::*()));
}


В этом примере мы дважны создаем алиасы с помощью using на четыре типа. Типы` U1` и U2, и типы их членов T1 и T2.
А дальше проверяем подстановку типов: какой из using-ов будет подставлен.


И подсказка есть в самом коде: четыре разных компилятора дадут четыре разных ответа.
Вот тут версия с запуском программ. И до сих пор дают разные варианты ответа....

Автор оригинала утверждает, что поведение четко определено в стандарте, и прав только gcc, а остальные по-разному врут...

Я ему верю, но код на всякий перекрещу, и святой водой ноутбук побрызгаю...
😁11🗿5🔥2👍1🎉1
Небольшой опрос про мою любимую тему - сраный move....

Есть у нас вот такой код:


template<class T>
inline constexpr T id(T x) { return x; }


int main() {
S y = id(id(S(42)));
}


У структуры S есть явно определенный noexcept move-конструктор.

Вопрос: сколько раз вызовется move-конструктор?

(ответ завтра)
Уточню флаги компиляции:


-O2 -std=c++20
Ответ: два раза.

А все почему? А вот почему:

У нас есть конкретные правила, когда copy-ellision разрешен, и когда явно запрещен. И там один абзац, и в абзаце этом черт ногу сломит. Нас интересует вот это


with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler)


Нам явно запрещено оптимизировать параметры функции. А вот return мы оптимизировать можем (RVO).

Вот такая вот интересная загогулина...

Из интересного, немного по-разному выглядит порядок деструкторов в MSVC и gcc/clang:


MSVC:
int-ctor
move-ctor
dtor
move-ctor
dtor
dtor

gcc:
int-ctor
move-ctor
move-ctor
dtor
dtor
dtor

clang:
int-ctor
move-ctor
move-ctor
dtor
dtor
dtor


Но ситуация еще забавнее. В предыдущий пост в комменты прилетело объявление move-конструктора с ключевым словом explicit.

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


struct S {
int value_;
explicit S(int i) : value_(i) { puts("int-ctor"); }
explicit S(S&&) noexcept { puts("move-ctor"); }
explicit S(S const &) noexcept { puts("cpy-ctor"); }
~S() { puts("dtor"); };
};


S id(S x) { return x; }
S id_cpy(const S& x) { return x; }



Вот так делать при этом нельзя:


error: class "S" has no suitable copy constructor


Что происходит. Это очень старая штука - компилятор сначала подставляет конструктор копирования, а уже потом применяет оптимизации (RVO, copy-ellision и т.п.), и тогда копирование уходит.

Мы можем в этом случае объявить функцию немного иначе:


S id(S x) { return S(x); }
S idm(S x) { return S(std::move(x)); }


В первом случае вызывается явно конструктор копирования, во втором - move конструктор.
При этом MSVC такие штуки в страшных муках вообще перестает компилировать.

gcc и clang компилируют, но не могут оптимизировать конструкторы по тем же самым причинам.



int-ctor
move-ctor
move-ctor
dtor
dtor
dtor


Но в таком примере, по любимой традиции, выпендрился icc. У него конструкторов 3, а деструкторов - 8.


int-ctor
move-ctor
dtor
move-ctor
dtor
dtor
dtor
dtor
dtor
dtor
dtor


И я вообще не понимаю, что в "голове" у icc, я с самого начала как натыкаюсь на что-то в icc - закрываю ноут и перестаю копать дальше....
😁8💊7🔥2🥴2
Я знаю много разных веселых реализаций TypeId - когда тебе надо по типу получить уникальный числовой идентефикатор.

Да что там, сам выступал с докладом, как такое сделать, и как мы это применили.

Но тут я нашел на еще одну реализацию, очень даже изящную (да, ты должен написать в комментарии, что Abseil это вообще-то классика жанра, а идея так вообще общеизвестна, и стыдно мне такое не знать)

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


namespace base_internal {
template <typename Type>
struct FastTypeTag {
static constexpr char kDummyVar = 0;
};
}


У нас есть шаблонный класс, в нем есть шаблонная константа. Для каждого типа Т, он объявит свою константу. Да, все константы будут равны нулю, но у каждой из них будет свой уникальный адрес (!)

А дальше дело техники - мы просто используем адрес в качестве идентефикатора.


template <typename Type>
constexpr FastTypeIdType FastTypeId() {
return FastTypeIdType(&base_internal::FastTypeTag<Type>::kDummyVar);
}


Это очень просто и изящно!

Ну и как многие другие способы получения Id типа - этот ломается в случае динамических библиотек. В каждой dll будет свой адрес константы и Id между разными dll будут расходиться.

Но идею я записал.
🔥174💅4
А вот в 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
Товарищи, а вы откуда в таком количестве подписываетесь сегодня? Кого благодарить за ссылку?
😁25👌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
Этот пост был в очереди где-то уже на май, но чет у меня настроение лирическое, публикую сейчас.