https://en.cppreference.com/w/cpp/filesystem/exists
Когда только начинаешь учить С++ после высокоуровневых языка самое забавное — насколько иногда простые вещи в этом языке сложно исполнять.
К примеру функция проверки существования файла на диске в стандартную библиотеку была добавлена только в стандарте 2017го года. До этого приходилось извращаться со всякими:
Когда только начинаешь учить С++ после высокоуровневых языка самое забавное — насколько иногда простые вещи в этом языке сложно исполнять.
К примеру функция проверки существования файла на диске в стандартную библиотеку была добавлена только в стандарте 2017го года. До этого приходилось извращаться со всякими:
ifstream f(name.c_str());
return f.good();
Еще одна проблема с переходом на низкоуровневые языки — операции с указателями (pointer).
Про это знают, наверно, все программисты, но иногда случаются ситуации, в которых с первого раза и не предположить подводных камней.
Рассмотрим следующий код:
Проблема данного кода в том, что вектор совершенно не гарантирует что данные будут все время лежать в одной и той же области памяти. В случае изменения размера — стандартная библиотека может переложить элементы в новую область памяти.
Давайте немного поменяем исходный код:
В худшем же случае данный код просто упадет в попытке произвести запись в ненадлежащий сегмент.
Про это знают, наверно, все программисты, но иногда случаются ситуации, в которых с первого раза и не предположить подводных камней.
Рассмотрим следующий код:
vector<int> v;Данный код выведет на экран значения:
v.push_back(1);
cout << v[0] << " ";
int *p = &*(--v.end());
(*p) = 2;
cout << v[0] << " ";
(*p) = 3;
cout << v[0] << " ";
1 2 3
Казалось бы все как мы и предполагали.Проблема данного кода в том, что вектор совершенно не гарантирует что данные будут все время лежать в одной и той же области памяти. В случае изменения размера — стандартная библиотека может переложить элементы в новую область памяти.
Давайте немного поменяем исходный код:
vector<int> v;В лучшем случае эта вариация выведет на экран:
v.reserve(1);
v.push_back(1);
cout << v[0] << " ";
int *p = &*(--v.end());
(*p) = 2;
cout << v[0] << " ";
v.reserve(3);
(*p) = 3;
cout << v[0] << " ";
1 2 2
, так как мы запишем последнее изменение в область памяти уже не имеющей отношения к вектору.В худшем же случае данный код просто упадет в попытке произвести запись в ненадлежащий сегмент.
Количество способов получить
Недавно я наткнулся на, кажется, самый идиотский из возможных способов которым хочу поделиться.
Допустим мы написали следующий код:
Казалось бы какие тут могут быть проблемы? И действительно структура города имеет размер
И действительно в данном коде проблем нет, давайте попробуем скомпилировать его с соответствующим санитайзером и убедиться в этом:
Дело в том, что примитивные типа должны записываться в ячейки с адресом значение которого целочисленно делится на выравнивание соответствующего типа.
Возьмем для примера long — выравнивание данного типа равно 8 байтам (на архитектуре x86-64), это значит что значение типа long должно начинаться с адресов делящихся на 8 (0, 8, 16, 24, etc). Тем не менее в мире С++ никто не останавливает нас от интерпретации любого случайного адреса памяти как long. Однако подобная интерпретация собственно и приводит к undefined behavior. Дело в том, что выравнивание памяти важно при низкоуровневых операциях, таких как подкачка кэша процессора. Соответственно неверное выравнивание может привести в дальнейшем к ошибкам в вычислениях.
Что же произошло в нашем случае?
В изначальной структуре long значение идентификатора города (в виде массива cityInfo) находилось в ячейке со смещением 0, что подходит под условия выравнивания. Однако после добавления int свойства (с размером в 4 байта), массив с информацией о городе сместился в ячейку 4, что собственно и привело к UB.
Что можно сделать в данном случае?
Мы можем вручную попросить компилятор использовать заданное смещение для свойства в структуре:
А что если мы не хотим тратить лишнюю память?
undefined behavior
в С++ порой поражает.Недавно я наткнулся на, кажется, самый идиотский из возможных способов которым хочу поделиться.
Допустим мы написали следующий код:
#include <iostream>Все казалось бы очевидно: у нас есть структура с информацией о каком то человеке (
struct Person {
char cityInfo[4];
};
struct City {
long id;
};
int main() {
Person me;
City *city = (City*) &me.cityInfo;
std::cout << city->id << std::endl;
return 0;
}
Person
), частью этой информации является байт массив с информацией о городе в котором человек живет (cityInfo
). Все что мы хотим сделать в данном коде — интерпретировать байт массив города в виде структуры (City
) и вывести на экран идентификатор этого города.Казалось бы какие тут могут быть проблемы? И действительно структура города имеет размер
sizeof(long)
, что в любой ситуации меньше sizeof(char[4]), а следовательно мы спокойно можем интерпретировать массив соответствующим образом.И действительно в данном коде проблем нет, давайте попробуем скомпилировать его с соответствующим санитайзером и убедиться в этом:
clang++ -fsanitize=undefined good_sample.cc && ./a.outТеперь давайте добавим в структуру с человеком информацию о его возрасте:
struct Person {Если же мы теперь попробуем перекомпилировать пример и выполнить его мы получим следующую ошибку:
int age;
char cityInfo[4];
};
clang++ -fsanitize=undefined ub_sample.cc && ./a.outВ чем же тут проблема? А проблема тут возникает в выравнивании памяти (alignment).
ub_sample.cc:17:24: runtime error: member access within misaligned address 0x7ffd07a42df4 for type 'City', which requires 8 byte alignment
0x7ffd07a42df4: note: pointer points here
f0 2e a4 07 fd 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 70 31 84 a6 7f 00 00
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub_sample.cc:17:24 in
ub_sample.cc:17:24: runtime error: load of misaligned address 0x7ffd07a42df4 for type 'long', which requires 8 byte alignment
0x7ffd07a42df4: note: pointer points here
f0 2e a4 07 fd 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 70 31 84 a6 7f 00 00
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub_sample.cc:17:24 in
32765
Дело в том, что примитивные типа должны записываться в ячейки с адресом значение которого целочисленно делится на выравнивание соответствующего типа.
Возьмем для примера long — выравнивание данного типа равно 8 байтам (на архитектуре x86-64), это значит что значение типа long должно начинаться с адресов делящихся на 8 (0, 8, 16, 24, etc). Тем не менее в мире С++ никто не останавливает нас от интерпретации любого случайного адреса памяти как long. Однако подобная интерпретация собственно и приводит к undefined behavior. Дело в том, что выравнивание памяти важно при низкоуровневых операциях, таких как подкачка кэша процессора. Соответственно неверное выравнивание может привести в дальнейшем к ошибкам в вычислениях.
Что же произошло в нашем случае?
В изначальной структуре long значение идентификатора города (в виде массива cityInfo) находилось в ячейке со смещением 0, что подходит под условия выравнивания. Однако после добавления int свойства (с размером в 4 байта), массив с информацией о городе сместился в ячейку 4, что собственно и привело к UB.
Что можно сделать в данном случае?
Мы можем вручную попросить компилятор использовать заданное смещение для свойства в структуре:
struct Person {По сути это будет равносильно добавлению еще одного int значения перед нашим массивом, компилятор просто выделит еще 4 байта между свойствами и проинициализирует их нулями.
int age;
char cityInfo[4] __attribute__ ((aligned (8)));
};
А что если мы не хотим тратить лишнюю память?
Продолжение.
В таком случае нам на помощь приходит std::bit_cast (https://en.cppreference.com/w/cpp/numeric/bit_cast), либо его аналог написанный вручную:
Все примеры вы можете найти тут: https://github.com/PatriosTheGreat/cppchannel/tree/main/ub_cast
В таком случае нам на помощь приходит std::bit_cast (https://en.cppreference.com/w/cpp/numeric/bit_cast), либо его аналог написанный вручную:
#include <cstdint>И хоть в данном случае мы и выделили на стеке память под новую структуру, в реальности в большинстве случаев компилятор может оптимизировать данное выделение.
#include <cstring>
#include <iostream>
struct Person {
int age;
char cityInfo[4];
};
struct City {
long id;
};
int main() {
Person me;
City city;
std::memcpy(&city, &me.cityInfo, sizeof(City));
std::cout << city.id << std::endl;
return 0;
}
Все примеры вы можете найти тут: https://github.com/PatriosTheGreat/cppchannel/tree/main/ub_cast
GitHub
cppchannel/ub_cast at main · PatriosTheGreat/cppchannel
Contribute to PatriosTheGreat/cppchannel development by creating an account on GitHub.
Грабли со static_cast
Недавно наткнулся на забавную багу в коде, которая заставила задуматься над поведением, казалось бы банального,
Давайте рассмотрим следующее дерево наследования:
Давайте теперь сделаем
В теории
Полный код доступен тут: https://godbolt.org/z/Yvr5oMd7h
Странным образом результатом такого вызова будет вполне корректный возврат значения 0. Впрочем если у вас опыта с плюсами побольше моего – для вас ничего странного в этом нет. Компилятор просто вставил вызов метода
Тут логично возникает два вопроса:
1. Что будет если метод foo станет виртуальным?
2. Что будет если метод foo решит использовать свойства класса
Ответ на первый вопрос я для краткости заметки отложу на следующий раз, а сейчас давайте попробуем использовать какое-нибудь свойство:
Полный код: https://godbolt.org/z/raccns5hs
Результатом подобного вызова будет считывание случайного куска памяти. Чтобы понять как это работает давайте посмотрим на ассемблер:
В данном случае в rbp лежит указатель на объект класса
По сути передав в этот метод объект класса
https://godbolt.org/z/b561Yajqa
И действительно результатом данной программы будет 42.
В следующий раз я попробую поиграться с виртуальными методами и посмотреть как компилятор будет вести себя в этом случае. А в качестве вывода статьи скажу, что если вы не уверены что лежит под указателем на родительский класс – пользуйтесь dynamic_cast, хоть это и дороже и требует RTTI.
Недавно наткнулся на забавную багу в коде, которая заставила задуматься над поведением, казалось бы банального,
static_cast
.Давайте рассмотрим следующее дерево наследования:
struct Base {};
struct ActualChild : Base {};
struct OtherChild : Base {};
Давайте теперь сделаем
static_cast
между братскими классами:ActualChild obj;
ActualChild* actual_ptr = &obj;
Base* base_ptr = actual_ptr;
OtherChild* other_ptr = static_cast<OtherChild*>(base_ptr);
В теории
static_cast
не должен делать ничего специфичного. Он просто интерпретирует память как объект данного типа. Пока что вроде особых проблем нет, но давайте добавим в класс OtherChild
метод и попробуем его вызвать:struct OtherChild : Base {
int foo() { return 0; }
};
—
other_ptr->foo();
Полный код доступен тут: https://godbolt.org/z/Yvr5oMd7h
Странным образом результатом такого вызова будет вполне корректный возврат значения 0. Впрочем если у вас опыта с плюсами побольше моего – для вас ничего странного в этом нет. Компилятор просто вставил вызов метода
OtherChild::foo()
и передал ему соответствующий указатель в качестве свойства this. Это можно и увидеть в окне с ассемблером в следующих строках:mov rdi, qword ptr [rbp - 32]
call OtherChild::foo()
Тут логично возникает два вопроса:
1. Что будет если метод foo станет виртуальным?
2. Что будет если метод foo решит использовать свойства класса
OtherClass
.Ответ на первый вопрос я для краткости заметки отложу на следующий раз, а сейчас давайте попробуем использовать какое-нибудь свойство:
struct OtherChild : Base {
int foo() { return value; }
int value;
};
…
other_ptr->foo();
Полный код: https://godbolt.org/z/raccns5hs
Результатом подобного вызова будет считывание случайного куска памяти. Чтобы понять как это работает давайте посмотрим на ассемблер:
mov rax, qword ptr [rbp - 8]
mov eax, dword ptr [rax]
В данном случае в rbp лежит указатель на объект класса
ActualChild
(this). Компилятор же в данном участке думает что он работает с OtherChild
. Для этого класса он знает, что значение value лежит в первых 4 байтах.По сути передав в этот метод объект класса
ActualChild
мы просто считаем память лежащую в первых 4 байт от этого указателя. Давайте собственно проверим так ли это следующим кодом:struct ValueHolder {
int value = 42;
};
int main() {
ValueHolder holder;
OtherChild* other_ptr =
static_cast<OtherChild*>(
(void*)&holder);
return other_ptr->foo();
}
https://godbolt.org/z/b561Yajqa
И действительно результатом данной программы будет 42.
В следующий раз я попробую поиграться с виртуальными методами и посмотреть как компилятор будет вести себя в этом случае. А в качестве вывода статьи скажу, что если вы не уверены что лежит под указателем на родительский класс – пользуйтесь dynamic_cast, хоть это и дороже и требует RTTI.
godbolt.org
Compiler Explorer - C++ (x86-64 clang 17.0.1)
struct Base {};
struct ActualChild : Base {};
struct OtherChild : Base {
int foo() { return 0; }
};
int main() {
ActualChild obj;
ActualChild* actual_ptr = &obj;
Base* base_ptr = actual_ptr;
OtherChild* other_ptr = static_cast<Other…
struct ActualChild : Base {};
struct OtherChild : Base {
int foo() { return 0; }
};
int main() {
ActualChild obj;
ActualChild* actual_ptr = &obj;
Base* base_ptr = actual_ptr;
OtherChild* other_ptr = static_cast<Other…
Продолжение граблей со static_cast
В прошлый раз мы выяснили что при касте к братскому классу компилятор вставит вызов метода того типа которым помечен объект.
Рассмотрим данный код:
https://godbolt.org/z/E1z194zf6
Результатом программы будет 2 и в окне ассемблера мы видим что компилятор вставил напрямую вызов
Давайте теперь посмотрим что будет в случае если метод станет виртуальным. Для этого нам надо поменять только структуру Base:
https://godbolt.org/z/1Kh8qfcfs
Теперь результатом будет 1.
Но каким образом компилятор определил какой метод вызвать? Ответ на это мы найдем в окне ассемблера в следующих строках (7-12 строки):
Последняя строка собственно является вызовом метода ActualChild::foo, однако как видно метод вызывается не напрямую, а через указатель на область памяти который лежит в регистре rax.
Давайте попробуем понять какой именно адрес лежит в этой строке. Как видно из второй строчки – адрес достается из регистра
Мы видим до 23й строки определенные операции которые нас не сильно интересуют. Далее мы видим вызов конструктора базового класса (строка 23). Затем в инструкции
После этого к адресу добавляется смещение в 16 (26 строка) и получившийся адрес собственно и кладется на стек, откуда мы его далее и получаем.
Зачем компилятору нужно смещение в 16? Для ответа на этот вопрос давайте собственно посмотрим что такое виртуальная таблица
Итак в первых 8 битах лежит 0 (честно говоря не знаю зачем, возможно это результат отсутствия оптимизаций, я собирал с O0). Далее лежит некоторая RTTI информация о классе, и после этого на расстоянии в 16 бит начинается список указателей на непосредственно виртуальные методы. Таким образом мы получаем указатель на
Дописав эту заметку, я вдруг понял, что по сути она не имеет ничего общего со static_cast. Я просто разобрал как работают таблицы виртуальных методов. Извиняюсь за небольшое вранье в названии.
В прошлый раз мы выяснили что при касте к братскому классу компилятор вставит вызов метода того типа которым помечен объект.
Рассмотрим данный код:
struct Base {};
struct ActualChild : Base {
int foo() { return 1; }
};
struct OtherChild : Base {
int foo() { return 2; }
};
int main() {
ActualChild actual;
OtherChild* other_ptr = static_cast<OtherChild*>((Base*)&actual);
return other_ptr->foo();
}
https://godbolt.org/z/E1z194zf6
Результатом программы будет 2 и в окне ассемблера мы видим что компилятор вставил напрямую вызов
OtherChild::foo()
(9я строка).Давайте теперь посмотрим что будет в случае если метод станет виртуальным. Для этого нам надо поменять только структуру Base:
struct Base {
virtual int foo() = 0;
};
https://godbolt.org/z/1Kh8qfcfs
Теперь результатом будет 1.
Но каким образом компилятор определил какой метод вызвать? Ответ на это мы найдем в окне ассемблера в следующих строках (7-12 строки):
call ActualChild::ActualChild() [base object constructor]
lea rax, [rbp - 16]
…
call qword ptr [rax]
Последняя строка собственно является вызовом метода ActualChild::foo, однако как видно метод вызывается не напрямую, а через указатель на область памяти который лежит в регистре rax.
Давайте попробуем понять какой именно адрес лежит в этой строке. Как видно из второй строчки – адрес достается из регистра
rbp
со смещением в 16. На всякий напомню что регистр rbp используется для хранения указателя на текущую голову стека. То есть конструктор ActualChild положил в стек указатель на соответствующую функцию foo. Давайте найдем как он это сделал. Для этого посмотрим на ассемблер данного конструктора (строки 16-30):ActualChild::ActualChild() [base object constructor]:
…
call Base::Base() [base object constructor]
…
lea rcx, [rip + vtable for ActualChild]
add rcx, 16
mov qword ptr [rax], rcx
add rsp, 16
pop rbp
ret
Мы видим до 23й строки определенные операции которые нас не сильно интересуют. Далее мы видим вызов конструктора базового класса (строка 23). Затем в инструкции
lea rcx, [rip + vtable for ActualChild]
мы видимо, что в регистр rcx кладется указатель на глобальный объект vtable_for_ActualChild
(rip
это специальная инструкция конкретно для Intel которая позволяет брать адреса объектов в глобальной области).После этого к адресу добавляется смещение в 16 (26 строка) и получившийся адрес собственно и кладется на стек, откуда мы его далее и получаем.
Зачем компилятору нужно смещение в 16? Для ответа на этот вопрос давайте собственно посмотрим что такое виртуальная таблица
vtable for ActualChild
в глобальной области. Для этого прокрутим ассемблер вниз до 48й строки где видим следующее:vtable for ActualChild:
.quad 0
.quad typeinfo for ActualChild
.quad ActualChild::foo()
Итак в первых 8 битах лежит 0 (честно говоря не знаю зачем, возможно это результат отсутствия оптимизаций, я собирал с O0). Далее лежит некоторая RTTI информация о классе, и после этого на расстоянии в 16 бит начинается список указателей на непосредственно виртуальные методы. Таким образом мы получаем указатель на
ActualChild::foo()
который конструктор ActualChild положит в сам объект и который будет далее вызван компилятором.Дописав эту заметку, я вдруг понял, что по сути она не имеет ничего общего со static_cast. Я просто разобрал как работают таблицы виртуальных методов. Извиняюсь за небольшое вранье в названии.
godbolt.org
Compiler Explorer - C++ (x86-64 clang 17.0.1)
struct Base {};
struct ActualChild : Base {
int foo() { return 1; }
};
struct OtherChild : Base {
int foo() { return 2; }
};
int main() {
ActualChild actual;
OtherChild* other_ptr = static_cast<OtherChild*>((Base*)&actual);
return other_ptr…
struct ActualChild : Base {
int foo() { return 1; }
};
struct OtherChild : Base {
int foo() { return 2; }
};
int main() {
ActualChild actual;
OtherChild* other_ptr = static_cast<OtherChild*>((Base*)&actual);
return other_ptr…