Первым постом в этом канале станет тот, с которого зародилось название канала.
Долгие разборы "военного синуса" привели меня к квинтессенции проблем, после которых я сумел сконструировать несколько абсолютно отвратительных примеров выстрелов себе в ногу.
Вот такой код, очевидно, не скомпилируется.
Что логично. Мы, конечно, объявили синус от строки, но в глобальном пространстве имен. А вызываем
Но давайте поменяем местами объявление синуса и заголовок
Угадайте что? Правильно: скомпилируется!
А все почему? Правильно, потому что в заголовке cmath (во многих реализациях на linux) есть вот такая строчка:
Будьте осторожны. Она там такая не одна.
Долгие разборы "военного синуса" привели меня к квинтессенции проблем, после которых я сумел сконструировать несколько абсолютно отвратительных примеров выстрелов себе в ногу.
#include <iostream>
#include <cmath>
double sin(std::string x) { return 1.5; }
int main() {
std::cout << "string: " << std::sin(std::string{"0.5"}) << std::endl;
}
Вот такой код, очевидно, не скомпилируется.
<source>:7:40: error: no matching function for call to 'sin(std::string)'
Что логично. Мы, конечно, объявили синус от строки, но в глобальном пространстве имен. А вызываем
std::sin из пространства имен std, и там такого синуса, очевидно, нет.Но давайте поменяем местами объявление синуса и заголовок
cmath. (можем считать, что у нас есть какой-то локальный заголовок, куда такое объявление просочилось)
#include <iostream>
double sin(std::string x) { return 1.5; }
#include <cmath>
int main() {
std::cout << "string: " << std::sin(std::string{"0.5"}) << std::endl;
}
Угадайте что? Правильно: скомпилируется!
Program returned: 0
string: 1.5
А все почему? Правильно, потому что в заголовке cmath (во многих реализациях на linux) есть вот такая строчка:
using ::sin;
Будьте осторожны. Она там такая не одна.
Telegram
this->notes.
#cpp
Ещё есть такой коллега у меня Паша Сухов. Ну как коллега. Он в Доставке вообще работает, но ведь это всё ещё Яндекс. Так что коллега.
Пашу вы могли видеть на C++ Russia:
- Полезные трюки С++ на примере организации пайплайна
- Как заставить шаблоны…
Ещё есть такой коллега у меня Паша Сухов. Ну как коллега. Он в Доставке вообще работает, но ведь это всё ещё Яндекс. Так что коллега.
Пашу вы могли видеть на C++ Russia:
- Полезные трюки С++ на примере организации пайплайна
- Как заставить шаблоны…
😱5🤯3🔥2👍1
Возвращаемся к нашему военному синусу.
Любимый пример, связанный с ним, после которого хочется переехать в комнату с мягкими стенами.
Эта программа нормально скомпилируется, на всех компиляторах, включая icc.
И выводит ожидаемые
Да, мы объявили какой-то синус в глобальном неймспейсе, который выдает какую-то дичь. Но мы вызываем синусы из стандартной библиотеки, значит все должно быть хорошо.
Ну да ладно, давайте ничего не заменим, только закомментируем последнюю строчку.
Что поменяется? Внезапно, поменяется поведение функции в предыдущей строки.
Как так? Почему эта строчка повлияет на поведение предыдущей? А вот, особенности порядка применения оптимизаций icc.
Добро пожаловать к нам в дурку.
Любимый пример, связанный с ним, после которого хочется переехать в комнату с мягкими стенами.
#include <iostream>
#include <cmath>
double
__attribute((weak))
sin(double x) { return 1.5; }
int main() {
std::cout << "double: " << std::sin(double(0.5)) << std::endl;
std::cout << "integer: " << std::sin(int(0)) << std::endl;
}
Эта программа нормально скомпилируется, на всех компиляторах, включая icc.
И выводит ожидаемые
Program returned: 0
double: 0.479426
integer: 0
Да, мы объявили какой-то синус в глобальном неймспейсе, который выдает какую-то дичь. Но мы вызываем синусы из стандартной библиотеки, значит все должно быть хорошо.
Ну да ладно, давайте ничего не заменим, только закомментируем последнюю строчку.
#include <iostream>
#include <cmath>
double
__attribute((weak))
sin(double x) { return 1.5; }
int main() {
std::cout << "double: " << std::sin(double(0.5)) << std::endl;
// std::cout << "integer: " << std::sin(int(0)) << std::endl;
}
Что поменяется? Внезапно, поменяется поведение функции в предыдущей строки.
Program returned: 0
double: 1.5
Как так? Почему эта строчка повлияет на поведение предыдущей? А вот, особенности порядка применения оптимизаций icc.
Добро пожаловать к нам в дурку.
Telegram
this->notes.
#cpp
Ещё есть такой коллега у меня Паша Сухов. Ну как коллега. Он в Доставке вообще работает, но ведь это всё ещё Яндекс. Так что коллега.
Пашу вы могли видеть на C++ Russia:
- Полезные трюки С++ на примере организации пайплайна
- Как заставить шаблоны…
Ещё есть такой коллега у меня Паша Сухов. Ну как коллега. Он в Доставке вообще работает, но ведь это всё ещё Яндекс. Так что коллега.
Пашу вы могли видеть на C++ Russia:
- Полезные трюки С++ на примере организации пайплайна
- Как заставить шаблоны…
😁9🔥5🤬3❤1
Микропостик.
Угадайте, что делает вот такой код?
Вот тут можно посмотреть ответ.
Этот код проверяет, является ли число нечетным.
Вывод
Только способом из дурки.
Угадайте, что делает вот такой код?
union S {
long long int number;
bool yes : sizeof(long long int);
};Вот тут можно посмотреть ответ.
#include <iostream>
union is_odd {
long long int number;
bool yes : sizeof(long long int);
};
int main() {
std::boolalpha(std::cout);
for (long long int i = -10; i < 10; ++i) {
std::cout << i << " is odd? "
<< is_odd{ .number = i }.yes
<< "\n";
}
}
Вывод
-10 is odd? false
-9 is odd? true
-8 is odd? false
-7 is odd? true
-6 is odd? false
-5 is odd? true
-4 is odd? false
-3 is odd? true
...
Только способом из дурки.
😁8❤3
Дисклеймер: я не претендую на невероятною новизну фактов. Я просто показываю всем известные примеры, на которые натыкаюсь раз за разом, раз за разом....
Вот еще один прекрасный пример такой гадости.
Мы сделаем указатели на две переменные. Если ставить оптимизацию до
Внимание, вопрос! Что будет, если мы сравним два одинаковых указателя?
И получаем внезапное:
И на самом деле - никаких противоречий со стандартом. Это просто Unspecified (даже не Undefined) Behavior.
Не буду раскрывать всю подноготную, легко этот пример найти где-нибудь на хабре.
Вопрос другой: вот когда на собеседовании в очередной раз задают вопросы типа "разверни список", или "проверь бинарное дерево на наличие циклов".
Вот что будет, если выдать в ответ собеседующему этот пример, и попросить доказать, что сравнение указателей в случае такого дерева/списка будет вообще работать? Без него же такую задачу не решить?
И посмотреть, как хорошо справится с ответом на этот вопрос тот, кто в очередной раз предложит развернуть список.
Хотя, разумеется, можно написать как-то так
Вот еще один прекрасный пример такой гадости.
#include<iostream>
int main(){
int a, b;
int* p = &a;
int* q = &b + 1;
std::cout << std::hex
<< "p: " << p << "\n"
<< "q: " << q << "\n"
<< std::endl;
}
Мы сделаем указатели на две переменные. Если ставить оптимизацию до
O1 включительно (после O2 начинаются вопросы на gcc) то переменные будут объявлены "по порядку", и это будут одинаковые указатели. Просто сравниваем посимвольно.
clang:
Program returned: 0
p: 0x7ffdfaf2696c
q: 0x7ffdfaf2696c
gcc:
Program returned: 0
p: 0x7ffca3076f0c
q: 0x7ffca3076f0c
Внимание, вопрос! Что будет, если мы сравним два одинаковых указателя?
#include<iostream>
int main(){
int a, b;
int* p = &a;
int* q = &b + 1;
std::cout << std::hex
<< "p: " << p << "\n"
<< "q: " << q << "\n"
<< std::endl;
std::cout << (p == q ? "equal" : "not equal")
<< std::endl;
}
И получаем внезапное:
clang:
Program returned: 0
p: 0x7ffd6bd0d16c
q: 0x7ffd6bd0d16c
equal
gcc:
Program returned: 0
p: 0x7ffe3c48696c
q: 0x7ffe3c48696c
not equal
И на самом деле - никаких противоречий со стандартом. Это просто Unspecified (даже не Undefined) Behavior.
Не буду раскрывать всю подноготную, легко этот пример найти где-нибудь на хабре.
Вопрос другой: вот когда на собеседовании в очередной раз задают вопросы типа "разверни список", или "проверь бинарное дерево на наличие циклов".
Вот что будет, если выдать в ответ собеседующему этот пример, и попросить доказать, что сравнение указателей в случае такого дерева/списка будет вообще работать? Без него же такую задачу не решить?
И посмотреть, как хорошо справится с ответом на этот вопрос тот, кто в очередной раз предложит развернуть список.
Хотя, разумеется, можно написать как-то так
int a, b;
auto p = reinterpret_cast<uintptr_t>(&a);
auto q = reinterpret_cast<uintptr_t>(&b + 1);
std::cout << std::hex << p << "\n" //
<< q << "\n" //
<< std::dec << (p == q) << std::endl; //
godbolt.org
Compiler Explorer - C++
int main(){
int a, b;
int* p = &a;
int* q = &b + 1;
std::cout << std::hex
<< "p: " << p << "\n"
<< "q: " << q << "\n"
<< std::endl;
}
int a, b;
int* p = &a;
int* q = &b + 1;
std::cout << std::hex
<< "p: " << p << "\n"
<< "q: " << q << "\n"
<< std::endl;
}
❤6🔥4🤯4👍1
Микропост.
Что выведет этот код?
Ответ вот тут.
А на самом деле надо бросать монетку, потому что gcc и msvc выведут
А clang и icc выведут
Но вообще, это просто типичный UB. p после реаллока становится невалидным указателем. Из хорошего - gcc это ловит со включенным.
Что выведет этот код?
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int*)malloc(sizeof(int));
int *q = (int*)realloc(p, sizeof(int));
if (p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
}
}
Ответ вот тут.
Program returned: 0
Program stdout
2 2
А clang и icc выведут
Program returned: 0
Program stdout
1 2
Но вообще, это просто типичный UB. p после реаллока становится невалидным указателем. Из хорошего - gcc это ловит со включенным
Wall😱2💯1
Итак, один из любимых примеров дурки.
Мы заводим структуру данных, которая конструируется от аргумента типа double. И вторую структуру, которая конструируется от первой.
А дальше создадим переменную второго типа.
Проигнорируем ворнинги компиляторов (кого оно волнует, компилируется же).
А что будет, если мы выведем значение перменной
Внезапно, выведет 1:
Чта?!
Ну, пришло время почитать ворнинги. И выясняем, что вот это:
не переменная. Это - функция. Дело в том, что вот два таких объявления функций, в целом, одинаковые:
Мы вполне легально можем аргументы функции заключать в скобки при объявлении функций... А все, что может быть трактовано в С++ как объявление функции, должно быть трактовано как объявление функции.
И.... Я не знаю, как оно так в итоге так получается, но стреляет время от времени...
Добро пожаловать к нам в дурку.
Мы заводим структуру данных, которая конструируется от аргумента типа 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));
Мы вполне легально можем аргументы функции заключать в скобки при объявлении функций... А все, что может быть трактовано в С++ как объявление функции, должно быть трактовано как объявление функции.
И.... Я не знаю, как оно так в итоге так получается, но стреляет время от времени...
Добро пожаловать к нам в дурку.
godbolt.org
Compiler Explorer - C++
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));
}
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));
}
🤯10❤4
У мну вопрос.
Есть вот такая строчка.
И вот в чем вопрос... В комментарии написано, что мы ждем от 2 до 6 секунд.
В функции мы берем минимум 1000 ms (одна секунда).
Мы берем по модулю 68 (не 60) и получаем 6.8 секунды.
Почему 68? Нахрена вообще понадобилась такая функция?
Столько вопросов... Но в этот раз не к языку. 🤡
Есть вот такая строчка.
// Waits for 2..6 seconds.
void SymbianEngine::updateConfigurationsAfterRandomTime()
{
int iTimeToWait = qMax(1000, (qAbs(qrand()) % 68) * 100);
#ifdef QT_BEARERMGMT_SYMBIAN_DEBUG
qDebug("QNCM waiting random time: %d ms", iTimeToWait);
#endif
QTimer::singleShot(iTimeToWait, this, SLOT(delayedConfigurationUpdate()));
}
И вот в чем вопрос... В комментарии написано, что мы ждем от 2 до 6 секунд.
В функции мы берем минимум 1000 ms (одна секунда).
Мы берем по модулю 68 (не 60) и получаем 6.8 секунды.
Почему 68? Нахрена вообще понадобилась такая функция?
Столько вопросов... Но в этот раз не к языку. 🤡
GitHub
qt/src/plugins/bearer/symbian/symbianengine.cpp at 92fde5feca3d792dfd775348ca59127204ab4ac0 · openwebos/qt
Qt for webOS. Contribute to openwebos/qt development by creating an account on GitHub.
🤡5🤷♂1😭1
Приколько поговорить про гадость, которую можно отловить на этапе компиляции.
Но есть разная дурка, которая аффектит рантайм, но проходит мимо всех ворнингов (иногда это ловят всякие clang-tidy и прочие PVS студии).
Вот канонический пример:
Мы создаем мапу (она будет создана в компайл тайме), а потом все объекты из нее эффективно муваются.
Но если мы внимательно посмотрим в исхродный код, то найдем там вот такое:
Я прошу прощения...? Какое копирование?! Где?
И вообще весь блок выглядит подозрительно похоже на конструктор копирования:
Не буду тут показывать, но, если провести перфоманс тесты, мы тоже увидим, что тут все тормозит.
И если детально разобраться: так и есть - это конструктор копирования.
Проблема вот в этой функции:
Если поменять объявление этой функции на
то этот блок строк на 70 машинного кода уйдет, а перфоманс выровняется.
Почему так? Если посмотреть на cpp reference, то мы увидим, что
значение в мапе содержит константный ключ. А конвертировать из структуры с константным ключем в структуру с неконстантным ключом.... Это копирование!...
И ни одного ворнинга! Мой же ты любимый С++.
Но есть разная дурка, которая аффектит рантайм, но проходит мимо всех ворнингов (иногда это ловят всякие 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>
значение в мапе содержит константный ключ. А конвертировать из структуры с константным ключем в структуру с неконстантным ключом.... Это копирование!...
И ни одного ворнинга! Мой же ты любимый С++.
godbolt.org
Compiler Explorer - C++
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"…
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"…
👍10😐5🔥2🗿2👌1
Внимание, шутка!!
Умножение в С++ некоммутативно.
Вот пример:
Выведет 5 только на первую строчку. А на вторую нет.
Живите с этим.
Разумеется, тут проблема в том, что в С++ числа с плавающей запятой пишутся с точкой. Запятая - это отдельныйоператор С++, который выводит второе значение.
Молодцы, что разгадали этот простенький паззл 🎁
К слову, clang выводит ворнинг на это. А gcc только при включенном Wall.
Умножение в С++ некоммутативно.
Вот пример:
#include <iostream>
int main() {
std::cout << (2,0 * 2,5) << std::endl; // 5
std::cout << (2,5 * 2,0) << std::endl; // ???
return 0;
}
Выведет 5 только на первую строчку. А на вторую нет.
Живите с этим.
Разумеется, тут проблема в том, что в С++ числа с плавающей запятой пишутся с точкой. Запятая - это отдельный
Молодцы, что разгадали этот простенький паззл 🎁
К слову,
godbolt.org
Compiler Explorer - C++
int main() {
std::cout << (2,0 * 2,5) << std::endl;
std::cout << (2,5 * 2,0) << std::endl;
return 0;
}
std::cout << (2,0 * 2,5) << std::endl;
std::cout << (2,5 * 2,0) << std::endl;
return 0;
}
❤7😁5🌚5🤓2
Немного про новые стандарты: когда я показываю какую-то упячку, часто мне говорят "просто не пользуйся вот этим...".
Например, "не пользуйся new/delete, пользуйся умными указателями".
Или "не пользуйся конструкторами с круглыми скобками, пользуйся {}. И порядок зафиксирует и от кучи проблем избавит.".
Увы, новые конструкции просто создают другое подмножество проблем.
Вот к примеру, что выведет вот такой код?
Правильно, он выведет
Что характерно, вот такая строчка
Не скомпилируется. Потому что у строки нет конструктора от char. Точнее есть, но там надо развлекаться веселее.
Это будет
Так или иначе, но у нас новые конструкции просто создают новые проблемы.
Ну и самая большая проблема, когда эти вещи вызываются неявно.
Человек, который прислал мне похожий пример, утверждает, что он пришел из продакшн кода.
Что тут происходит? double кастится к int, int кастится к char, char кастится к string, в мапу записывается буква
Например, "не пользуйся 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. Прекрасно!godbolt.org
Compiler Explorer - C++
int main() {
auto a = std::string{48};
std::cout << a << std::endl;
// auto b = std::string(48); // error: no matching function for call to
}
auto a = std::string{48};
std::cout << a << std::endl;
// auto b = std::string(48); // error: no matching function for call to
}
🥴12🔥3😢3❤2🤩1
implicit конструирование типов вообще сплошная головная боль.
Но мой любимый лефтикус в одном из своих докладов (чет не помню, в каком, но в последние пару лет) поделился синтаксическим трюком, как их запретить.
Вот у нас есть пример.
Мы хотим как-то запускать функцию
У нас, казалось бы, есть защита от неправильного порядка аргументов.
Но есть проблема. Оба типа могут неявно конструироваться от строки. Вот эти обе строчки сработают:
Есть решение!Нам поможет вот такая конструкция:
```cpp
void f(const auto&, const auto&) = delete;
void f(const Path& path, const std::string& mode) {
std::cout << "Path: " << path.path << std::endl;
std::cout << "Mode: " << mode << std::endl;
}
```
Что мы теперь получим при вызове функций от голых строк?
```
<source>:18:5: error: call to deleted function 'f'
18 | f("/etc/tmp", "rw");
```
Прекрасный трюк, очень мне нравится.
Но мой любимый лефтикус в одном из своих докладов (чет не помню, в каком, но в последние пару лет) поделился синтаксическим трюком, как их запретить.
Вот у нас есть пример.
struct Path {
Path(const std::string& p) : path(p) {}
Path(const char* p) : path(p) {}
std::string path;
};
void f(const Path& path, const std::string& mode) {
std::cout << "Path: " << path.path << std::endl;
std::cout << "Mode: " << mode << std::endl;
}
Мы хотим как-то запускать функцию
f.У нас, казалось бы, есть защита от неправильного порядка аргументов.
f(Path("/etc/tmp"), std::string{"rw"});
// f(std::string{"rw"}, Path("/etc/tmp"));
// error: no matching function for call to 'f'
Но есть проблема. Оба типа могут неявно конструироваться от строки. Вот эти обе строчки сработают:
f("/etc/tmp", "rw");
f("rw", "/etc/tmp");
Есть решение!
```cpp
void f(const auto&, const auto&) = delete;
void f(const Path& path, const std::string& mode) {
std::cout << "Path: " << path.path << std::endl;
std::cout << "Mode: " << mode << std::endl;
}
```
Что мы теперь получим при вызове функций от голых строк?
```
<source>:18:5: error: call to deleted function 'f'
18 | f("/etc/tmp", "rw");
```
Прекрасный трюк, очень мне нравится.
🔥14🤔2
Ну а как еще можно было назвать канал, как не "Дурка"?
Ну сам факт, что вот такой код компилируется, запускается, а еще и не падает, и выводит 0!!!
Нет, конечно, понятно даже почему, если разобрать поднаготную устройства функций-членов класса. Но блииииин........
Ну сам факт, что вот такой код компилируется, запускается, а еще и не падает, и выводит 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();
}
Нет, конечно, понятно даже почему, если разобрать поднаготную устройства функций-членов класса. Но блииииин........
godbolt.org
Compiler Explorer - C++
class Test {
public:
void test() {
std::cout << this << std::endl;
}
};
Test & create()
{
return *((Test*)NULL);
}
int main()
{
Test &t = create();
t.test();
}
public:
void test() {
std::cout << this << std::endl;
}
};
Test & create()
{
return *((Test*)NULL);
}
int main()
{
Test &t = create();
t.test();
}
💊9😨2❤1👍1👎1😢1
Неожиданный пример гонки в С++.
Возьмем такой код.
Что мы тут видим?
Класс Task с чисто виртуальной функцией. Интерфейс, типичный интерфейс.
Промежуточный класс AutorunTask, который в конструкторе запускает в конструкторе виртуальный метод, и в деструкторе дожидается его окончания.
И в конце концов реализация виртуальной функции, которая и вызывается.
И в таком виде код работает, и выводит
Но зачем мы поставили sleep в конце? Дело в том, что если его закомментировать, то код упадет с ошибкой.
Итак. Давайте разбираться.
Можно ли вообще в конструкторе/деструкторе использовать виртуальные функции?
Смотрим
paragraph 4 [ISO/IEC 14882-2014]
Тоесть, можно.
Что же они запускают? Ведь функция в классе-наследнике может обращаться к несконструированному классу?
На самом деле, в конструкторе и деструкторе вызывается реализация "текущего" класса. Смотрим пример.
Это код выводит
конструктор первого класса выведет A. Второго B.
А как это достигается? На самом деле, в программе переписывается указатель в таблице виртуальных вызовов.
Ооооо, вот тут и возникает гонка.
Вот тут перед непосредственным вызовом деструктора устанавливается другой указатель в таблице виртальных вызовов.
А вызов внутри лямбды происходит не конкретной функции, а по указателю из таблицы виртуальных вызовов.
И дальше происходит гонка. Если деструктор вызовется раньше, чем запустится поток (версия без `sleep`), то вызовется чистовиртуальный метод, что приведет к ошибке.
А если деструктор вызывается позже (версия со `sleep`), то все отработает штатно.
Добро пожаловать в красивые примеры нашей любимой дурки. 🤡
Возьмем такой код.
struct Task {
virtual ~Task() = default;
virtual void run() = 0;
};
class AutorunTask : public Task {
public:
AutorunTask() : t_([this]() {
this->run();
}) {}
~AutorunTask() { t_.join(); }
private:
std::thread t_;
};
struct Impl : public AutorunTask {
void run() override {
puts("The string");
}
};
int main(){
Impl impl;
std::this_thread::sleep_for(1ms);
}
Что мы тут видим?
Класс Task с чисто виртуальной функцией. Интерфейс, типичный интерфейс.
Промежуточный класс AutorunTask, который в конструкторе запускает в конструкторе виртуальный метод, и в деструкторе дожидается его окончания.
И в конце концов реализация виртуальной функции, которая и вызывается.
И в таком виде код работает, и выводит
Program returned: 0
The string
Но зачем мы поставили sleep в конце? Дело в том, что если его закомментировать, то код упадет с ошибкой.
Program returned: 139
pure virtual method called
terminate called without an active exception
Program terminated with signal: SIGSEGV
Итак. Давайте разбираться.
Можно ли вообще в конструкторе/деструкторе использовать виртуальные функции?
Смотрим
paragraph 4 [ISO/IEC 14882-2014]
Member functions, including virtual functions, can be called during construction or destruction. When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class. If the virtual function call uses an explicit class member access and the object expression refers to the complete object of x or one of that object’s base class subobjects but not x or one of its base class subobjects, the behavior is undefined.
Тоесть, можно.
Что же они запускают? Ведь функция в классе-наследнике может обращаться к несконструированному классу?
На самом деле, в конструкторе и деструкторе вызывается реализация "текущего" класса. Смотрим пример.
struct A {
virtual void f() {
puts("A");
}
A() { f(); }
};
struct B : public A {
public:
void f() override {
puts("B");
}
B() { f(); }
};
int main(){
B b;
}
Это код выводит
Program returned: 0
A
B
конструктор первого класса выведет A. Второго B.
А как это достигается? На самом деле, в программе переписывается указатель в таблице виртуальных вызовов.
Ооооо, вот тут и возникает гонка.
AutorunTask::~AutorunTask() [base object destructor]:
push rbx
mov rbx, rdi
lea rax, [rip + vtable for AutorunTask+16]
mov qword ptr [rdi], rax
add rbx, 8
mov rdi, rbx
call std::thread::join()@PLT
Вот тут перед непосредственным вызовом деструктора устанавливается другой указатель в таблице виртальных вызовов.
vtable for AutorunTask:
.quad 0
.quad typeinfo for AutorunTask
.quad AutorunTask::~AutorunTask() [base object destructor]
.quad AutorunTask::~AutorunTask() [deleting destructor]
.quad __cxa_pure_virtual
А вызов внутри лямбды происходит не конкретной функции, а по указателю из таблицы виртуальных вызовов.
std::thread::_State_impl<std::thread::_Invoker<std::tuple<AutorunTask::AutorunTask()::'lambda'()>>>::_M_run() [complete object constructor]:
mov rdi, qword ptr [rdi + 8]
mov rax, qword ptr [rdi]
jmp qword ptr [rax + 16]
И дальше происходит гонка. Если деструктор вызовется раньше, чем запустится поток (версия без `sleep`), то вызовется чистовиртуальный метод, что приведет к ошибке.
А если деструктор вызывается позже (версия со `sleep`), то все отработает штатно.
Добро пожаловать в красивые примеры нашей любимой дурки. 🤡
godbolt.org
Compiler Explorer - C++
using namespace std::chrono_literals;
struct Task {
virtual ~Task() = default;
virtual void run() = 0;
};
class AutorunTask : public Task {
public:
AutorunTask() : t_([this]() {
this->run();
}) {}
~AutorunTask() { t_.join();…
struct Task {
virtual ~Task() = default;
virtual void run() = 0;
};
class AutorunTask : public Task {
public:
AutorunTask() : t_([this]() {
this->run();
}) {}
~AutorunTask() { t_.join();…
❤11🤯8🫡4👍3
Иногда нам приходится использовать самое страшное и отвратительное что досталось С++ по наследству: препроцессор.
По разным причинам - или средствами языка нельзя что-то емко выразить (как в либах Unreal Engine), или для производительности (иногда голый код лучше оптимизируется чем функции, и макросы пишутся вместо функций под комментарием "счастливого дебага, тварь"), иногда просто по дурости (как в QT).
И там можно выстрелить себе в ногу самым неожиданным образом.
Вот простой макрос:
Что тут не так?
Да много чего.
Вот такой пример:
Вообще это преобразуется в
И это явно не то, что мы хотели. Мы хотели тут ошибку компиляции на том, что "пытаемся инкрементить lvalue", а вместо этого простой ворнинг про "неиспользуемый результат".
Но это полбеды, тут даже идейно макрос использован неверно.
Какой-нибудь вот такой пример:
Разумно выведет
а clang даже со включенным Wall не выдаст ни одного ворнинга.
И это далеко не все проблемы. Когда-то давно, когда мне надо было писать макросы, мне к задачам выдали 18-страничный гайд "как правильно писать макросы", согласно которому единственно верным способом написать такой макрос будет
Не используйте макросы без крайней необходимости. А если используете - найдите тот гайд, и пришлите его мне (я его потерял и очень об этом жалею 🙁 )
По разным причинам - или средствами языка нельзя что-то емко выразить (как в либах Unreal Engine), или для производительности (иногда голый код лучше оптимизируется чем функции, и макросы пишутся вместо функций под комментарием "счастливого дебага, тварь"), иногда просто по дурости (как в QT).
И там можно выстрелить себе в ногу самым неожиданным образом.
Вот простой макрос:
#define INCREMENT_BOTH(x, y) x++; y++
Что тут не так?
Да много чего.
Вот такой пример:
int a = 0; int b = 0;
INCREMENT_BOTH(a+b, b);
Вообще это преобразуется в
a+b++; b++;
И это явно не то, что мы хотели. Мы хотели тут ошибку компиляции на том, что "пытаемся инкрементить lvalue", а вместо этого простой ворнинг про "неиспользуемый результат".
Но это полбеды, тут даже идейно макрос использован неверно.
Какой-нибудь вот такой пример:
int main() {
int a = 0; int b = 0;
for (const auto f: {true, false}) {
if (f)
INCREMENT_BOTH(a, b);
}
std::cout << "a: " << a << "\n"
<< "b: " << b << "\n"
<< std::endl;
return 0;
}
Разумно выведет
Program returned: 0
a: 1
b: 2
а clang даже со включенным Wall не выдаст ни одного ворнинга.
И это далеко не все проблемы. Когда-то давно, когда мне надо было писать макросы, мне к задачам выдали 18-страничный гайд "как правильно писать макросы", согласно которому единственно верным способом написать такой макрос будет
#define INCREMENT_BOTH(x, y) \
do { \
(x)++; \
(y)++; \
} while (0)
Не используйте макросы без крайней необходимости. А если используете - найдите тот гайд, и пришлите его мне (я его потерял и очень об этом жалею 🙁 )
godbolt.org
Compiler Explorer - C++
#define INCREMENT_BOTH(x, y) x++; y++
int main() {
int a = 0; int b = 0;
for (const auto f: {true, false}) {
if (f)
INCREMENT_BOTH(a, b);
}
std::cout << "a: " << a << "\n"
<< "b: " << b << "\n"
…
int main() {
int a = 0; int b = 0;
for (const auto f: {true, false}) {
if (f)
INCREMENT_BOTH(a, b);
}
std::cout << "a: " << a << "\n"
<< "b: " << b << "\n"
…
❤14
Еще одна абсолютно бесполезная, но унаследнованная штука в С++. Заголовок может заинклюдить сам себя.
Например вот такой заголовок:
Сработает совершенно нормально.
Ни ворнингов, ничего. А самое главное - как бы так передефайнить разные куски, чтобы на этом механизме устроить перебор? Ну, перебрать все комбинации из 5 дефайнов, и для каждого определить функцию? Было бы... Забавно?...
Например вот такой заголовок:
// 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❤1👍1🥴1
(незапланированный пост)
А есть инфа, я что-то найти не могу.
Вот это в язык вводят?
А есть инфа, я что-то найти не могу.
Вот это в язык вводят?
template <class P, class Q>
auto dot_product(P p, Q q) {
// no indirection!
auto&& [...p_elems] = p;
auto&& [...q_elems] = q;
return (... + (p_elems * q_elems));
}
🤯5❤1