Строгая гарантия исключений
#новичкам
Базовая гарантия - это конечно хорошо, наше приложение будет корректно работать, даже если что-то пойдет не так. Но иногда этого недостаточно. Иногда нам нужно, чтобы ошибка операции вообще никак не повлияла на текущее состояние системы. Либо операция выполнилась и все хорошо, либо она бросила исключение, но после его отлова система находится в том же состоянии, что и до выполнения операции.
Такое свойство операций называется транзакционность. Транзакция может либо выполниться полностью, либо все результаты промежуточных операций в ней откатываются до состояния до начала исполнения транзакции.
Это важно, когда ваша операция требует выполнения нескольких промежуточных операций, постепенно меняющих систему. Если остановиться посередине, то уже невозможно или очень сложно будет восстановить консистентность данных.
Давайте перепишем оператор присваивания класс IntArray из предыдущего поста так, чтобы он предоставлял строгую гарантию:
В этот раз мы ничего не изменяем в самом объекте до тех пор, пока не выделим новый буфер и не скопируем туда элементы
Хрестоматийный пример из стандартной библиотеки - вектор с его методом push_back. Если у типа есть небросающий перемещающий конструктор, то метод предоставляет строгую гарантию. Вот примерно как это работает:
в хэлпере reallocate используется std::move_if_noexcept, который условно кастит в rvalue ссылке, если мув конструктор noexcept. И только в этом случае можно предоставить строгую гарантию: если вы уже повредили один из исходных объектов, его уже никак не восстановить. А безопасное перемещение элементов гарантирует готовый к использованию новый расширенный буфер.
Be strong. Stay cool.
#cppcore
#новичкам
Базовая гарантия - это конечно хорошо, наше приложение будет корректно работать, даже если что-то пойдет не так. Но иногда этого недостаточно. Иногда нам нужно, чтобы ошибка операции вообще никак не повлияла на текущее состояние системы. Либо операция выполнилась и все хорошо, либо она бросила исключение, но после его отлова система находится в том же состоянии, что и до выполнения операции.
Такое свойство операций называется транзакционность. Транзакция может либо выполниться полностью, либо все результаты промежуточных операций в ней откатываются до состояния до начала исполнения транзакции.
Это важно, когда ваша операция требует выполнения нескольких промежуточных операций, постепенно меняющих систему. Если остановиться посередине, то уже невозможно или очень сложно будет восстановить консистентность данных.
Давайте перепишем оператор присваивания класс IntArray из предыдущего поста так, чтобы он предоставлял строгую гарантию:
class IntArray {
int *array;
std::size_t nElems;
public:
// ...
~IntArray() { delete[] array; }
IntArray(const IntArray &that); // nontrivial copy constructor
IntArray &operator=(const IntArray &rhs) {
int *tmp = nullptr;
if (rhs.nElems) {
tmp = new int[rhs.nElems];
std::memcpy(tmp, rhs.array, rhs.nElems * sizeof(*array));
}
delete[] array;
array = tmp;
nElems = rhs.nElems;
return *this;
}
// ...
};В этот раз мы ничего не изменяем в самом объекте до тех пор, пока не выделим новый буфер и не скопируем туда элементы
rhs. И только после этого выполняем обновление самого объекта с помощью небросающих инструкций.Хрестоматийный пример из стандартной библиотеки - вектор с его методом push_back. Если у типа есть небросающий перемещающий конструктор, то метод предоставляет строгую гарантию. Вот примерно как это работает:
template <typename T>
class vector {
private:
T *data = nullptr;
size_t size = 0;
size_t capacity = 0;
void reallocate(size_t new_capacity) {
// allocate memory
T *new_data =
static_cast<T *>(::operator new(new_capacity * sizeof(T)));
size_t new_size = 0;
try {
// Move or copy elements
for (size_t i = 0; i < size; ++i) {
new (new_data + new_size) T(std::move_if_noexcept(data[i]));
++new_size;
}
} catch (...) {
// Rollback in case of exception
for (size_t i = 0; i < new_size; ++i) {
new_data[i].~T();
}
::operator delete(new_data);
throw;
}
// cleanup
// ...
}
public:
void push_back(const T &value) {
if (size >= capacity) {
size_t new_capacity = capacity == 0 ? 1 : capacity * 2;
// save for rollback
T *old_data = data;
size_t old_size = size;
size_t old_capacity = capacity;
try {
reallocate(new_capacity);
} catch (...) {
// restore
data = old_data;
size = old_size;
capacity = old_capacity;
throw;
}
}
// actually insert element
// ...
}
};
в хэлпере reallocate используется std::move_if_noexcept, который условно кастит в rvalue ссылке, если мув конструктор noexcept. И только в этом случае можно предоставить строгую гарантию: если вы уже повредили один из исходных объектов, его уже никак не восстановить. А безопасное перемещение элементов гарантирует готовый к использованию новый расширенный буфер.
Be strong. Stay cool.
#cppcore
1❤14🔥8👍7👎3
Гарантия отсутствия исключений
#новичкам
Переходим к самой сильной гарантии - отсутствие исключений.
В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.
Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.
Но если такой термин есть, значит такие гарантии можно предоставлять для отдельных сущностей. Давайте как раз об этих сущностях и поговорим.
Но для начала проясним термины.
Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.
nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.
Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).
nofail же подразумевает полное отсутствие ошибок. nofail гарантия ожидается от std::swap, мув-конструкторов классов и других функций с помощью которых достигается строгая гарантия исключений.
Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.
nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.
В языке в целом эти гарантии обеспечиваются ключевым словом noexcept. При появлении этой нотации компилятор понимает, что для этой функций не нужно генерировать дополнительный код, необходимый для обработки исключений. Но у этого есть своя цена: если из noexcept функции вылетит исключение, то сразу же без разговоров вызовется std::terminate.
Provide guarantees. Stay cool.
#cppcore #cpp11
#новичкам
Переходим к самой сильной гарантии - отсутствие исключений.
В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.
Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.
Но если такой термин есть, значит такие гарантии можно предоставлять для отдельных сущностей. Давайте как раз об этих сущностях и поговорим.
Но для начала проясним термины.
Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.
nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.
Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).
nofail же подразумевает полное отсутствие ошибок. nofail гарантия ожидается от std::swap, мув-конструкторов классов и других функций с помощью которых достигается строгая гарантия исключений.
Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.
nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.
В языке в целом эти гарантии обеспечиваются ключевым словом noexcept. При появлении этой нотации компилятор понимает, что для этой функций не нужно генерировать дополнительный код, необходимый для обработки исключений. Но у этого есть своя цена: если из noexcept функции вылетит исключение, то сразу же без разговоров вызовется std::terminate.
Provide guarantees. Stay cool.
#cppcore #cpp11
❤15👍8🔥5❤🔥3😁3👎1
WAT
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
"Век живи - век учись" - сказал Луций Сенека.
"Век живи - век учи С++" - реалии нашей жизни.
Просто посмотрите на следующий код:
И он компилируется.
WAT?
Это называется injected class name. Имя класса доступно из скоупа этого же класса. Так сделано для того, чтобы поиск имени
Такое поведение может быть полезно в таком сценарии:
injected class name гарантирует, что из метода
Это также полезно внутри шаблонов классов, где имя класса можно использовать без списка аргументов шаблона, например, используя просто Foo вместо полного идентификатора шаблона Foo<blah, blah, blah>.
Ну и побочным эффектом такого поведения является возможность написания длиннющей цепочки из имен класса.
Так что это не у вас в глазах двоится, это плюсы такие шебутные)
Find yourself within. Stay cool.
#cppcore
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
"Век живи - век учись" - сказал Луций Сенека.
"Век живи - век учи С++" - реалии нашей жизни.
Просто посмотрите на следующий код:
struct Foo
{
void Bar();
};
void Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Bar()
{
printf("Foofoo!");
}
int main()
{
Foo f;
f.Bar();
return 0;
}
И он компилируется.
WAT?
Это называется injected class name. Имя класса доступно из скоупа этого же класса. Так сделано для того, чтобы поиск имени
X внутри класса X всегда разрешался именно в этот класс.Такое поведение может быть полезно в таком сценарии:
void X() { }
class X {
public:
static X Сreate() { return X(); }
};injected class name гарантирует, что из метода
Сreate будет возвращен именно инстанс класса Х, а не результат вызова функции Х.Это также полезно внутри шаблонов классов, где имя класса можно использовать без списка аргументов шаблона, например, используя просто Foo вместо полного идентификатора шаблона Foo<blah, blah, blah>.
Ну и побочным эффектом такого поведения является возможность написания длиннющей цепочки из имен класса.
Так что это не у вас в глазах двоится, это плюсы такие шебутные)
Find yourself within. Stay cool.
#cppcore
10🔥36🤯11❤9😁6⚡4👍4
data race
#новичкам
Конкретных проблем, которые можно допустить в многопоточной среде, существует оооочень много. Но все они делятся на несколько больших категорий. В этом и следующих постах мы на примерах разберем основные виды.
Начнем с data race. Это по сути единственная категория, которая четко определена в стандарте С++.
Скажем, что два обращения к памяти конфликтуют, если:
- они обращаются к одной и той же ячейке памяти.
- по крайней мере одно из обращений - запись.
Так вот гонкой данных называется 2 конфликтующих обращения к неатомарной переменной, между которыми не возникло отношение порядка "Произошло-Раньше".
Если не вдаваться в семантику отношений порядков, то отсутствие синхронизации с помощью примитивов(мьютексов и атомиков) при доступе к неатомикам карается гонкой данных и неопределененным поведением.
Простой пример:
В двух потоках пытаемся инкрементировать
Гонку данных относительно несложно определить по коду, просто следую стандарту, да и тред-санитайзеры, пользуясь определением гонки, могут ее детектировать. Поэтому как будто бы эта не самая основная проблема в многопоточке. Существуют другие, более сложные в детектировании и воспроизведении.
Have an order. Stay cool.
#cppcore #concurrency
#новичкам
Конкретных проблем, которые можно допустить в многопоточной среде, существует оооочень много. Но все они делятся на несколько больших категорий. В этом и следующих постах мы на примерах разберем основные виды.
Начнем с data race. Это по сути единственная категория, которая четко определена в стандарте С++.
Скажем, что два обращения к памяти конфликтуют, если:
- они обращаются к одной и той же ячейке памяти.
- по крайней мере одно из обращений - запись.
Так вот гонкой данных называется 2 конфликтующих обращения к неатомарной переменной, между которыми не возникло отношение порядка "Произошло-Раньше".
Если не вдаваться в семантику отношений порядков, то отсутствие синхронизации с помощью примитивов(мьютексов и атомиков) при доступе к неатомикам карается гонкой данных и неопределененным поведением.
Простой пример:
int a = 0;
void thread_1() {
for (int i = 0; i < 10000; ++i) {
++a;
}
}
void thread_2() {
for (int i = 0; i < 10000; ++i) {
++a;
}
}
std::jthread thr1{thread_1};
std::jthread thr1{thread_2};
std::cout << a << std::endl;
В двух потоках пытаемся инкрементировать
a. Проблема в том, что при выводе на консоль a не будет равна 20000, а скорее всего чуть меньшему числу. Инкремент инта - это неатомарная операция над неатомиком, поэтому 2 потока за счет отсутствия синхронизации кэшей будут читать и записывать неактуальные данные.Гонку данных относительно несложно определить по коду, просто следую стандарту, да и тред-санитайзеры, пользуясь определением гонки, могут ее детектировать. Поэтому как будто бы эта не самая основная проблема в многопоточке. Существуют другие, более сложные в детектировании и воспроизведении.
Have an order. Stay cool.
#cppcore #concurrency
❤32😁15👍11🔥3👎1
Самая надежная гарантия отсутствия исключений
#опытным
Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать try-catch блоки. Исключения - это не zero-cost абстракция. throw требуют динамические аллокации, catch - RTTI, а в машинном коде компилятор обязан генерировать инструкции на случай вылета исключений. Плюс обработка исключений сама по себе медленная.
Поэтому некоторые и стараются минимизировать использование исключений и максимально использовать noexcept код.
Но можно решить проблему накорню. Так сказать отрезать ее корешок под самый корешок.
Есть такой флаг компиляции -fno-exceptions. Он запрещает использование исключений в программе. Но что значит запрет на использование исключений?
👉🏿 Ошибка компиляции при выбросе исключения. А я говорил, что под корень рубим. Вы просто не соберете программу, которая кидает исключения.
👉🏿 Ошибка компиляции при попытке обработать исключение. Ну а че, если вы живете в мире без исключений, зачем вам их обрабатывать?
👉🏿 Можно конечно сколько угодно жить в розовом мире без исключений, но рано или поздно придется использовать чужой код. Что будет, если он выкинет исключение?
Моментальное завершение работы. Оно как бы и понятно. Метод мапы at() кидает std::out_of_range исключение, если ключа нет в мапе. Обрабатывать исключение нельзя, поэтому чего вола доить, сразу терминируемся. И никакой вам раскрутки стека и graceful shutdown. Просто ложимся и умираем, скрестив ручки.
То есть вы накорню запрещаете упоминание исключений в вашем коде, а если что-то пошло не по плану, то оно пойдет по п...
Зато получаете стабильно высокую производительность и предсказуемый флоу программы.
Как тогда код писать? А об этом через пару постов.
Handle errors. Stay cool.
#cppcore #compiler
#опытным
Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать try-catch блоки. Исключения - это не zero-cost абстракция. throw требуют динамические аллокации, catch - RTTI, а в машинном коде компилятор обязан генерировать инструкции на случай вылета исключений. Плюс обработка исключений сама по себе медленная.
Поэтому некоторые и стараются минимизировать использование исключений и максимально использовать noexcept код.
Но можно решить проблему накорню. Так сказать отрезать ее корешок под самый корешок.
Есть такой флаг компиляции -fno-exceptions. Он запрещает использование исключений в программе. Но что значит запрет на использование исключений?
👉🏿 Ошибка компиляции при выбросе исключения. А я говорил, что под корень рубим. Вы просто не соберете программу, которая кидает исключения.
int main() {
throw 1; // even this doesn't compile
}👉🏿 Ошибка компиляции при попытке обработать исключение. Ну а че, если вы живете в мире без исключений, зачем вам их обрабатывать?
int main() {
// even this doesn't compile
try {
} catch(...) {
}
}👉🏿 Можно конечно сколько угодно жить в розовом мире без исключений, но рано или поздно придется использовать чужой код. Что будет, если он выкинет исключение?
std::map<int, int> map;
std::cout << map.at(1) << std::endl;
Моментальное завершение работы. Оно как бы и понятно. Метод мапы at() кидает std::out_of_range исключение, если ключа нет в мапе. Обрабатывать исключение нельзя, поэтому чего вола доить, сразу терминируемся. И никакой вам раскрутки стека и graceful shutdown. Просто ложимся и умираем, скрестив ручки.
То есть вы накорню запрещаете упоминание исключений в вашем коде, а если что-то пошло не по плану, то оно пойдет по п...
Зато получаете стабильно высокую производительность и предсказуемый флоу программы.
Как тогда код писать? А об этом через пару постов.
Handle errors. Stay cool.
#cppcore #compiler
👍27❤11🔥6😁3❤🔥2🤔1
Что не так с модулями?
#опытным
Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.
Модули - это новая фундаментальная единица организации кода, которая должна дополнить и в идеале(в мечтах комитета) заменить старую концепцию заголовочных файлов.
Если по простому, то модуль - это такой бинарный черный ящик, у которого четко определен интерфейс, который он экспортирует наружу.
Экспортируемые сущности явно помечаются в коде модуля. Затем модуль компилируется и из бинарного его представления можно дергать только эти экспортируемые сущности.
Короткий пример:
и его использование:
Модули призваны решать следующие проблемы:
✅ Одни и те же заголовки могут сотни раз обрабатываться компилятором при компиляции программ из многих единиц трансляции. Модули же компилируются один раз, в них кэшируется информация, необходимая для нормальной компиляции cpp файлов и потом эта информация просто используется при компиляции. Никакой повторной работы!
Это значит, что время компиляции должно заметно уменьшиться.
✅ В хэдэрах зачастую нужно оставлять некоторые детали реализации, которые не нужны пользователю, но нужны для корректной компиляции. Модули же явно экспортируют только нужный интерфейс.
✅ Никакой макросятины! Ни один макрос не прошмыгнет внутрь клиентского кода из модуля, потому что он уже скомпилирован.
На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...
А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?
😡 Модули - довольно сложная штука в реализации. Не будем вдаваться в нюансы, но компилятор должен сильно измененить свое поведение и преобрести свойства системы сборки, чтобы нормально компилировать модули. А делать они этого не хотят. Плюс многие компиляторы опенсорсные и не так-то просто в опенсорсе реализовывать такие масштабные идеи. На винде с этим попроще, потому что во главе всего Microsoft и они завезли модули раньше всех.
😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.
😡 Из-за этого в том числе хромает тулинг. Дело в том, что модуль - это бинарный файл и программист просто так не может, например, посмотреть сигнатуру метода в каком-то файле. Это большая проблема, которую должны решить редакторы и анализаторы кода. Но отсутствие стандартизации формата мешает интеграции модулей в них.
😡 Очень много усилий нужно потратить на переработку архитектуры и кода существующих проектов, чтобы перевести их на модули.
😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.
😡 Нужны новейшие версии систем сборки, компиляторов и других инструментов, чтобы заработали модули.
😡 Пока популярные библиотеки не начнут распространяться через модули, существующие проекты не будут иметь большое желание переезжать на модули, потому что получится частичное внедрение.
Тем не менее, если у вас есть самые актуальные инструменты, вы запускаете новый проект или решили в тестовом режиме обновлять уже существующий, то пользоваться модулями уже можно, хоть и осторожно и с ожиданием возможных проблем.
Use new features. Stay cool.
#cppcore #compiler #tools
#опытным
Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.
Модули - это новая фундаментальная единица организации кода, которая должна дополнить и в идеале(в мечтах комитета) заменить старую концепцию заголовочных файлов.
Если по простому, то модуль - это такой бинарный черный ящик, у которого четко определен интерфейс, который он экспортирует наружу.
Экспортируемые сущности явно помечаются в коде модуля. Затем модуль компилируется и из бинарного его представления можно дергать только эти экспортируемые сущности.
Короткий пример:
// math.cppm - файл модуля
export module math; // Объявление модуля
import <vector>; // Импорт, а не включение
// Макросы НЕ экспортируются!
#define PI 3.14159
// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);
// Внутренние функции скрыты
void internal_helper();
и его использование:
// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода
// Используем экспортированную функцию
double area = calculate_circle_area(10);
// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются
Модули призваны решать следующие проблемы:
✅ Одни и те же заголовки могут сотни раз обрабатываться компилятором при компиляции программ из многих единиц трансляции. Модули же компилируются один раз, в них кэшируется информация, необходимая для нормальной компиляции cpp файлов и потом эта информация просто используется при компиляции. Никакой повторной работы!
Это значит, что время компиляции должно заметно уменьшиться.
✅ В хэдэрах зачастую нужно оставлять некоторые детали реализации, которые не нужны пользователю, но нужны для корректной компиляции. Модули же явно экспортируют только нужный интерфейс.
✅ Никакой макросятины! Ни один макрос не прошмыгнет внутрь клиентского кода из модуля, потому что он уже скомпилирован.
На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...
А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?
😡 Модули - довольно сложная штука в реализации. Не будем вдаваться в нюансы, но компилятор должен сильно измененить свое поведение и преобрести свойства системы сборки, чтобы нормально компилировать модули. А делать они этого не хотят. Плюс многие компиляторы опенсорсные и не так-то просто в опенсорсе реализовывать такие масштабные идеи. На винде с этим попроще, потому что во главе всего Microsoft и они завезли модули раньше всех.
😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.
😡 Из-за этого в том числе хромает тулинг. Дело в том, что модуль - это бинарный файл и программист просто так не может, например, посмотреть сигнатуру метода в каком-то файле. Это большая проблема, которую должны решить редакторы и анализаторы кода. Но отсутствие стандартизации формата мешает интеграции модулей в них.
😡 Очень много усилий нужно потратить на переработку архитектуры и кода существующих проектов, чтобы перевести их на модули.
😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.
😡 Нужны новейшие версии систем сборки, компиляторов и других инструментов, чтобы заработали модули.
😡 Пока популярные библиотеки не начнут распространяться через модули, существующие проекты не будут иметь большое желание переезжать на модули, потому что получится частичное внедрение.
Тем не менее, если у вас есть самые актуальные инструменты, вы запускаете новый проект или решили в тестовом режиме обновлять уже существующий, то пользоваться модулями уже можно, хоть и осторожно и с ожиданием возможных проблем.
Use new features. Stay cool.
#cppcore #compiler #tools
❤18👍7🔥6
pointer to data member
#опытным
В этом посте мы рассказывали о том, что с помощью ranges и и параметра проекции можно кастомизировать алгоритмы с соответствии с определенным полем класса. Например, чтобы найти в коллекции элемент с максимальным определенным полем, то можно сделать так:
max в этом случае будет транзакцией с максимальным размером платежа.
В последней строчке используется
Если про указатели на конкретные мемберы знают не только лишь все, то это совсем дебри плюсов.
Явный тип указателя на поле класса используется так:
По сути это особый тип указателя, который хранит смещение поля относительно начала объекта в байтах. Это не специфицировано в стандарте, но примерно везде так работает.
Мы обязательно должны указать, на какой тип полей этот указатель может указывать. Таким образом указатель
Указателю на интовое поле нельзя присвоить указатель на флотовое. И наоборот, указатель
Если вы подумали, что очень узкоспециализированная вещь, то вы правы. Чуть больше универсализации здесь могут дать шаблоны:
Walk through the nooks and crannies. Stay cool.
#cppcore #memory
#опытным
В этом посте мы рассказывали о том, что с помощью ranges и и параметра проекции можно кастомизировать алгоритмы с соответствии с определенным полем класса. Например, чтобы найти в коллекции элемент с максимальным определенным полем, то можно сделать так:
struct Payment {
double amount;
std::string category;
};
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);max в этом случае будет транзакцией с максимальным размером платежа.
В последней строчке используется
&Payment::amount - указатель на поле amount в классе Payment. Но если это параметр функции, то это значение какого-то типа. Но какой тип у этого указателя?Если про указатели на конкретные мемберы знают не только лишь все, то это совсем дебри плюсов.
Явный тип указателя на поле класса используется так:
struct Payment {
double amount;
std::string category;
};
double Payment::*ptr = &Payment::amount; // Here!
Payment payment{3.14, "Groceries"};
std::cout << payment.*ptr << std::endl;
// OUTPUT:
// 3.14double Payment::* ptr = &Payment::amount;
// тип указателя имя указателя инициализатор
По сути это особый тип указателя, который хранит смещение поля относительно начала объекта в байтах. Это не специфицировано в стандарте, но примерно везде так работает.
Мы обязательно должны указать, на какой тип полей этот указатель может указывать. Таким образом указатель
ptr может указывать на любое поле класса Payment, имеющее тип double. То есть:struct Type {
int a;
int b;
float c;
};
int Type::*p = nullptr;
p = &Type::a; // OK, a is int
p = &Type::b; // OK, b is int
p = &Type::c; // ERROR! c is floatУказателю на интовое поле нельзя присвоить указатель на флотовое. И наоборот, указатель
p работает с любыми полями типа int.Если вы подумали, что очень узкоспециализированная вещь, то вы правы. Чуть больше универсализации здесь могут дать шаблоны:
// Takes pointer to any data member for any type
template<typename T, typename FieldType>
void print_field(const T& obj, FieldType T::*field) {
std::cout << obj.*field << std::endl;
}
Payment payment{3.14, "Groceries"};
Type t(42, 69, 3.14);
print_field(payment, &Payment::amount);
print_field(payment, &Payment::category);
print_field(t, &Type::a);
print_field(t, &Type::b);
print_field(t, &Type::c);
// OUTPUT
// 3.14
// Groceries
// 42
// 69
// 3.14
print_field может печатать значение любого поля любого класса по его указателю. Обратите внимание на шаблонную сигнатуру.Walk through the nooks and crannies. Stay cool.
#cppcore #memory
3❤24🔥15👍11
WAT
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Всегда ли nullptr указатель равен нулю?
Казалось бы в названии дан ответ:
Но в общем случае это неправда! Смотрим на пример:
nullptr указатель равен совсем не нулю, как декларировалось в начале main.
WAT? Что за фокусы с пропажей нуля?
Во вчерашнем посте мы рассказывали об особом типе указателя - pointer to data member. Этот указатель, которым и является
И в большинстве случаев эта информация представляет собой просто смещение поля относительно начала объекта в байтах.
Однако нулевое смещение используется для локации самого первого поля класса. Поэтому в байтовом представлении неинициализированный указатель не может быть нулем.
Вместо этого обычно используется число -1, которое в байтовом представлении как раз выглядит как все единички:
С помощью указателей на поля класса можно кстати наглядно изучать выравнивание и упаковку полей с объект:
Опять же, интересный уголок плюсов.
Walk through the nooks and crannies. Stay cool.
#cppcore #memory
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Всегда ли nullptr указатель равен нулю?
Казалось бы в названии дан ответ:
int * p = nullptr;
std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
// OUTPUT
// true
// 0
Но в общем случае это неправда! Смотрим на пример:
struct A {
int i;
};
int main() {
int A::* p = 0;
std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
std::cout << std::boolalpha << (std::bit_cast<std::uintptr_t>(p) == 0xffffffffffffffff) << "\n";
}
// OUTPUT:
// true
// ffffffffffffffff
// truenullptr указатель равен совсем не нулю, как декларировалось в начале main.
WAT? Что за фокусы с пропажей нуля?
Во вчерашнем посте мы рассказывали об особом типе указателя - pointer to data member. Этот указатель, которым и является
p из примера, по сути хранит информацию о том, как в объекте найти нужное поле класса.И в большинстве случаев эта информация представляет собой просто смещение поля относительно начала объекта в байтах.
Однако нулевое смещение используется для локации самого первого поля класса. Поэтому в байтовом представлении неинициализированный указатель не может быть нулем.
Вместо этого обычно используется число -1, которое в байтовом представлении как раз выглядит как все единички:
std::cout << std::hex << static_cast<long long int>(-1) << "\n";
// OUTPUT:
// ffffffffffffffff
С помощью указателей на поля класса можно кстати наглядно изучать выравнивание и упаковку полей с объект:
struct Type {
double a;
char b;
float c;
long long d;
short e;
unsigned f;
};
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::a) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::b) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::c) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::d) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::e) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::f) << "\n";
// OUTPUT:
// 0
// 8
// 12
// 16
// 24
// 28Опять же, интересный уголок плюсов.
Walk through the nooks and crannies. Stay cool.
#cppcore #memory
❤22👍12🔥9🤯4❤🔥2
Сколько весит объект полиморфного класса?
#новичкам
Частый вопрос с собеседований про размеры объектов различных классов. Даже в бэкэндерских конторах, в которых никогда в жизни не учитывали эти размеры. Но такие вопросы раскрывают знание базы, а считается, что без ее знания вы не можете писать нормальный код.
У нас уже был пост про размер объекта пустого класса.
А что если это будет класс с виртуальными методами? Сколько тогда будет весить этот класс?
Мы знаем, что для каждого класса, имеющего виртуальные методы, создается глобальная таблица виртуальных функций. В ней находятся конкретные адреса виртуальных методов конкретно этого класса. И именно к ней обращаются, когда хотят порешать вопросики, какой метод вызвать.
Таблица одна на все объекты заданного класса. И им каким-то образом нужно получить доступ к этой таблице.
Учитывайте, что нельзя захардкодить эту информацию по статическому типу объекта(например SomeClass& или SomeClass*), потому что под его личиной может скрываться наследник.
Значит надо ее класть в каждый объект. И самое простое - положить в них указатель на свою таблицу виртуальных функций. Так и делают на самом деле. Этот указатель называют vptr.
Соответственно размер класса зависит от битности системы. Для 64-бит указатель имеет размер 8 байт(64 бита) поэтому и размер класса SomeClass будет 8 байт.
Пустые наследники SomeClass кстати тоже будут иметь размер 8 из-за того, что им нужно лишь другое значения указателя.
Если вы добавите еще полей, то размер увеличится в соответствии с размером дополнительных полей и выравниванием. Если вы используете множественное наследование, то тоже увеличится, но об этом как-нибудь потом поговорим.
Be lightweight. Stay cool.
#cppcore #interview
#новичкам
Частый вопрос с собеседований про размеры объектов различных классов. Даже в бэкэндерских конторах, в которых никогда в жизни не учитывали эти размеры. Но такие вопросы раскрывают знание базы, а считается, что без ее знания вы не можете писать нормальный код.
У нас уже был пост про размер объекта пустого класса.
А что если это будет класс с виртуальными методами? Сколько тогда будет весить этот класс?
struct SomeClass {
virtual ~SomeClass() = default;
virtual void Process() {
std::cout << "Process" << std::endl;
}
};Мы знаем, что для каждого класса, имеющего виртуальные методы, создается глобальная таблица виртуальных функций. В ней находятся конкретные адреса виртуальных методов конкретно этого класса. И именно к ней обращаются, когда хотят порешать вопросики, какой метод вызвать.
Таблица одна на все объекты заданного класса. И им каким-то образом нужно получить доступ к этой таблице.
Учитывайте, что нельзя захардкодить эту информацию по статическому типу объекта(например SomeClass& или SomeClass*), потому что под его личиной может скрываться наследник.
Значит надо ее класть в каждый объект. И самое простое - положить в них указатель на свою таблицу виртуальных функций. Так и делают на самом деле. Этот указатель называют vptr.
Соответственно размер класса зависит от битности системы. Для 64-бит указатель имеет размер 8 байт(64 бита) поэтому и размер класса SomeClass будет 8 байт.
std::cout << sizeof(SomeClass) << std::endl;
// OUTPUT
// 8
Пустые наследники SomeClass кстати тоже будут иметь размер 8 из-за того, что им нужно лишь другое значения указателя.
Если вы добавите еще полей, то размер увеличится в соответствии с размером дополнительных полей и выравниванием. Если вы используете множественное наследование, то тоже увеличится, но об этом как-нибудь потом поговорим.
Be lightweight. Stay cool.
#cppcore #interview
1❤22👍15🔥7😁7🆒1
enum class
#новичкам
Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы
В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:
Он решает две большие проблемы обычных перечислений:
👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.
Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:
Что значит прибавить красному цвету единицу - решительно непонятно.
Неявные преобразования enum class'ов же запрещено:
Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:
👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:
И все прекрасно компилируется.
С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.
Protect your scope. Stay cool.
#cppcore #cpp11
#новичкам
Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы
В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:
enum class Enumeration {CATEGORY1, CATEGORY2, CATEGORY3};Он решает две большие проблемы обычных перечислений:
👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.
Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:
cpp
enum Color { RED, GREEN, BLUE };
Color c = RED;
Color next = c + 1; // Implicit conversion to int and visa versa!
Что значит прибавить красному цвету единицу - решительно непонятно.
Неявные преобразования enum class'ов же запрещено:
enum class Color { RED, GREEN, BLUE };
Color c = Color::RED;
Color next = c + 1; // ERROR!Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:
Color c = Color::RED;
Color next = static_cast<Color>(static_cast<int>(c) + 1);
👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
enum Color { RED, GREEN, BLUE };
enum TrafficLight { RED, YELLOW, GREEN }; // ERROR!
void graphics_library() {
Color c = RED;
}У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:
enum class Color1 { RED, GREEN, BLUE };
enum class Color2 { RED, GREEN, BLUE };
void graphics_library() {
Color1 c1 = Color1::RED;
Color2 c2 = Color2::RED;
}И все прекрасно компилируется.
С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.
Protect your scope. Stay cool.
#cppcore #cpp11
👍42❤16🔥16
Мувать не всегда дешево
#новичкам
С приходом мув семантики настали "прекрасные плюсы будущего". Нет никакого копирования, чудо-оптимизации бороздят просторы стека и кучи. Не жизнь, а сказка.
Но мир не такой уж солнечный и приветливый. Это очень опасное...
Если вы придерживаетесь RAII, пользуетесь контейнерами и умными указателями, то вы практически всегда пользуетесь правилом нуля и никогда не определяете самостоятельно специальные методы класса и, в частности, конструктор перемещения и оператор перемещающего присваивания. Компилятор сгенерирует их за вас, ленивых дядь.
Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.
И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:
Что выбрать?
"Конечно мувнуть, это же не долгое копирование, выполнится быстро" - вот к таким не совсем корректным мыслям может привести "оторванность от низов".
Кажется, что у некоторых людей есть ощущение, что данные из одного объекта как-то перетекают в другой объект и это происходит очень быстро.
Но это не так! Перемещение - это поверхностное копирование.
Возьмем простой пример:
Что будет происходить при перемещении
Чуть сложнее:
Что будет при перемещении
Можно еще занулить конечно, но это редко происходит из соображений перфоманса.
Получается, что реально "переместить" вы можете только данные, выделенные на куче. И то они никуда не перемещаются. Вы просто копируете указатель из одного объекта в другой, при этом сами данные никак не затрагиваются.
Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.
Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.
Теперь снова актуализируем вопрос: мувать или копировать?
И ответ уже не плоскости оптимизации, а в плоскости логики кода. Перемещайте, когда вам в текущем скоупе объект больше не нужен и копируйте, если нужен. Тогда вы не пытаетесь оптимизировать код, а передаете владение объектом другому коду. Редко, когда вы на авито продаете вещи, чтобы заработать. Вы их продаете, чтобы от лишнего избавиться и дать их тем, кому они нужны, особой выгоды не ожидая. Вот здесь примерно это и должно происходить.
В реальности все немного сложнее и всегда будут исключения, но просто хочу обратить внимание, что мув семантика - это в первую очередь про передачу владения объектом и только потом уже оптимизация.
Think logically. Stay cool.
#cppcore #cpp11
#новичкам
С приходом мув семантики настали "прекрасные плюсы будущего". Нет никакого копирования, чудо-оптимизации бороздят просторы стека и кучи. Не жизнь, а сказка.
Но мир не такой уж солнечный и приветливый. Это очень опасное...
Если вы придерживаетесь RAII, пользуетесь контейнерами и умными указателями, то вы практически всегда пользуетесь правилом нуля и никогда не определяете самостоятельно специальные методы класса и, в частности, конструктор перемещения и оператор перемещающего присваивания. Компилятор сгенерирует их за вас, ленивых дядь.
Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.
И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:
void Process(Data data);
Что выбрать?
"Конечно мувнуть, это же не долгое копирование, выполнится быстро" - вот к таким не совсем корректным мыслям может привести "оторванность от низов".
Кажется, что у некоторых людей есть ощущение, что данные из одного объекта как-то перетекают в другой объект и это происходит очень быстро.
Но это не так! Перемещение - это поверхностное копирование.
Возьмем простой пример:
struct Data {
int a;
double b;
};
Data obj1{3, 3.14};
Data obj2 = std::move(obj1);Что будет происходить при перемещении
obj1? Копирование a и b.Чуть сложнее:
struct Data {
std::array<int, 5> arr;
};
Data obj1{.arr = {1, 2, 3, 4, 5}};
Data obj2 = std::move(obj1);Что будет при перемещении
obj1, а значит и arr? Тоже копирование! std::array - это массив, фиксированного размера, расположенный на стеке. Как вы собираетесь его перемещать в другой объект? Под другой объект уже выделена своя память на стеке, вы не можете один кусок стека переместить в другой. Вы можете только скопировать значения.Можно еще занулить конечно, но это редко происходит из соображений перфоманса.
Получается, что реально "переместить" вы можете только данные, выделенные на куче. И то они никуда не перемещаются. Вы просто копируете указатель из одного объекта в другой, при этом сами данные никак не затрагиваются.
struct Data {
std::string * str;
// member functions for making it work properly
};
Data obj1{.str = new std::string("Hello, World!")};
Data obj2 = std::move(obj1);obj2 теперь имеет такое же значение указателя str, как и obj1, но сама строка оказалась нетронутой.Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.
Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.
Теперь снова актуализируем вопрос: мувать или копировать?
И ответ уже не плоскости оптимизации, а в плоскости логики кода. Перемещайте, когда вам в текущем скоупе объект больше не нужен и копируйте, если нужен. Тогда вы не пытаетесь оптимизировать код, а передаете владение объектом другому коду. Редко, когда вы на авито продаете вещи, чтобы заработать. Вы их продаете, чтобы от лишнего избавиться и дать их тем, кому они нужны, особой выгоды не ожидая. Вот здесь примерно это и должно происходить.
В реальности все немного сложнее и всегда будут исключения, но просто хочу обратить внимание, что мув семантика - это в первую очередь про передачу владения объектом и только потом уже оптимизация.
Think logically. Stay cool.
#cppcore #cpp11
3👍38❤15🔥6😎5
Передача владения
#новичкам
Захотелось совсем немного развить тему предыдущего поста.
В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.
Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.
Другой пример:
Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.
Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.
Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.
Поэтому можно принять за правило, что, передав владение, вы больше физически не имеете права пользоваться объектом. Это как продав компанию, вы бы продолжили иметь то же влияние на нее. Нетушки. Либо крестик снимите, либо трусы наденьте. Либо передали владение и забыли, либо скопировали и дальше попользовались.
Give away what you don't need. Stay cool.
#cppcore #cpp11
#новичкам
Захотелось совсем немного развить тему предыдущего поста.
В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.
void bar(std::vector<int>&& vec) {
// do nothing
}
void foo() {
std::vector<int> vec = {1, 2, 3};
bar(std::move(vec));
}Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.
Другой пример:
std::vector<int> double_elements(std::vector<int> vec) {
for (auto& elem: vec) {
elem *= 2;
}
return vec;
}
void foo() {
std::vector<int> vec = {1, 2, 3};
{
auto doubled = double_elements(vec);
std::println("{}", doubled);
}
vec.push_back(4);
{
auto doubled = double_elements(std::move(vec));
std::println("{}", doubled);
}
}
Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.
Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.
Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.
Поэтому можно принять за правило, что, передав владение, вы больше физически не имеете права пользоваться объектом. Это как продав компанию, вы бы продолжили иметь то же влияние на нее. Нетушки. Либо крестик снимите, либо трусы наденьте. Либо передали владение и забыли, либо скопировали и дальше попользовались.
Give away what you don't need. Stay cool.
#cppcore #cpp11
❤18👍12🔥6
Атрибуты везде
#опытным
Используют атрибуты функций не только лишь все, мало кто знает, куда их можно пихать.
Есть на самом деле 3 легальных места для навешивания атрибутов на функцию.
1️⃣ Перед типом возвращаемого значения:
Тогда он работает при непосредственном использовании функции.
2️⃣ После имени функции:
В таком виде атрибут тоже применяется к самой функции.
3️⃣ После параметров:
Тогда атрибут применяется к типу функции, а не к самой функции. Разница вот в чем:
Обычный вызов функции прекрасно компилируется. Но вот использование типа функции через decltype помечается как устаревшее.
Причем gcc и clang по-разному интерпретируют эту ситуацию. Clang говорит, что gnu::deprecated нельзя применять к типам и игнорирует атрибут. Вот ссылка на годболт для интересующихся.
Соответственно, в лямбде в тех же местах можно ставить атрибуты:
Признавайтесь, знали?)
Have your own opinion. Stay cool.
#cppcore
#опытным
Используют атрибуты функций не только лишь все, мало кто знает, куда их можно пихать.
Есть на самом деле 3 легальных места для навешивания атрибутов на функцию.
1️⃣ Перед типом возвращаемого значения:
[[deprecated]] int foo() { return 42; }Тогда он работает при непосредственном использовании функции.
foo();
// warning: 'int foo()' is deprecated
2️⃣ После имени функции:
int foo [[deprecated]] () { return 42; }В таком виде атрибут тоже применяется к самой функции.
3️⃣ После параметров:
int foo() [[gnu::deprecated]] { return 42; }Тогда атрибут применяется к типу функции, а не к самой функции. Разница вот в чем:
int foo() [[gnu::deprecated]] { return 42; }
int main() {
foo(); // no warnings
using FuncType = decltype(foo); // use of type is deprecated
}Обычный вызов функции прекрасно компилируется. Но вот использование типа функции через decltype помечается как устаревшее.
Причем gcc и clang по-разному интерпретируют эту ситуацию. Clang говорит, что gnu::deprecated нельзя применять к типам и игнорирует атрибут. Вот ссылка на годболт для интересующихся.
Соответственно, в лямбде в тех же местах можно ставить атрибуты:
auto complicated_compute = [] [[nodiscard]] () [[gnu::deprecated]] {
return 2 * 2;
};Признавайтесь, знали?)
Have your own opinion. Stay cool.
#cppcore
👍19🤯18❤9🔥6
Атрибуты параметров функции
#новичкам
Атрибуты можно также применять к параметрам функции. Это помогает чуть полнее в коде функции аннотировать некоторые свойства параметров.
Вы определили какой-то интерфейс с методом, принимающим один параметр. И в какой-то момент появилась необходимость создать наследника, реализующего этот интерфейс, однако реализации не нужен параметр param. Возможно Implementation - это какой-то мок, у которого в принципе пустая реализация.
Если вы активно используете варнинги компилятора и прочие линтеры, при попытке собрать такой код вы скорее всего увидите предупреждение/ошибку компиляции. Чтобы стало все чётенько, стоит пометить
Однако из стандартных атрибутов по сути имеет смысл использовать только этот самый maybe_unused.
Но атрибуты - это не только средство общения с компилятором. Это еще и средство налаживания коммуникации между автором кода и его пользователями/читателями.
Например:
Вы поместили в хэдэр такое объявление, тем самым явно сказав пользователю и компилятору, что указатели не должны быть нулевыми. Если компилятор докажет в compile-time, что в функцию передали nullptr, то он выкинет предупреждение. Ну а пользователь четко по сигнатуре видит, что функция не ожидает нулевой указатель и как порядочный гражданин не будет его передавать.
Annotate your code. Stay cool.
#cppcore
#новичкам
Атрибуты можно также применять к параметрам функции. Это помогает чуть полнее в коде функции аннотировать некоторые свойства параметров.
class Interface {
public:
virtual void method(int param) = 0;
};
class Implementation : public Interface {
public:
void method(int param) override {
// this implementation doesn't use param so mark it
}
};Вы определили какой-то интерфейс с методом, принимающим один параметр. И в какой-то момент появилась необходимость создать наследника, реализующего этот интерфейс, однако реализации не нужен параметр param. Возможно Implementation - это какой-то мок, у которого в принципе пустая реализация.
Если вы активно используете варнинги компилятора и прочие линтеры, при попытке собрать такой код вы скорее всего увидите предупреждение/ошибку компиляции. Чтобы стало все чётенько, стоит пометить
param атрибутом maybe_unused, тем самым явно указав компилятору, что параметр не используется намеренно. И проблема исчезнет.Однако из стандартных атрибутов по сути имеет смысл использовать только этот самый maybe_unused.
Но атрибуты - это не только средство общения с компилятором. Это еще и средство налаживания коммуникации между автором кода и его пользователями/читателями.
Например:
size_t safe_strcpy(
[[gnu::nonnull]] char* dest,
[[gnu::nonnull]] const char* src,
size_t dest_size
);
Вы поместили в хэдэр такое объявление, тем самым явно сказав пользователю и компилятору, что указатели не должны быть нулевыми. Если компилятор докажет в compile-time, что в функцию передали nullptr, то он выкинет предупреждение. Ну а пользователь четко по сигнатуре видит, что функция не ожидает нулевой указатель и как порядочный гражданин не будет его передавать.
Annotate your code. Stay cool.
#cppcore
❤13🔥11👍7🤯1