#include <filename> vs #include "filename"
#новичкам
Тот нюанс, который зеленые программисты С++ впитывают из окружающей среды, но зачастую не понимают его деталей.
Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?
Стандарт нам дает отписку, что поведение при обоих подходах implementation defined, поэтому надо смотреть на то, как ведут себя большинство компиляторов.
Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.
У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.
Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).
А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.
В конце концов, обычно, в обоих случаях все известные компилятору директории будут просмотрены на наличие подходящего файла, пока он не будет найден. Вопрос только в порядке поиска.
Так что в большинстве случаев разницы особо никакой не будет, кроме времени компиляции. Однако все равно есть определенные гайдлайны, которым следуют большинство разработчиков.
✅ Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.
✅ Используем #include "filename" для включения заголовочных файлов своего проекта. Препроцессор будет сначала искать эти файлы там, где вы ему об этом явно укажите с помощью опций.
See the difference. Stay cool.
#cppcore #compiler
#новичкам
Тот нюанс, который зеленые программисты С++ впитывают из окружающей среды, но зачастую не понимают его деталей.
Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?
Стандарт нам дает отписку, что поведение при обоих подходах implementation defined, поэтому надо смотреть на то, как ведут себя большинство компиляторов.
Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.
У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.
Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).
А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.
В конце концов, обычно, в обоих случаях все известные компилятору директории будут просмотрены на наличие подходящего файла, пока он не будет найден. Вопрос только в порядке поиска.
Так что в большинстве случаев разницы особо никакой не будет, кроме времени компиляции. Однако все равно есть определенные гайдлайны, которым следуют большинство разработчиков.
✅ Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.
#include <stdio.h> // Стандартный заголовочник
#include <curl/curl.h> // Заголовочник из системной директории
int main(void) {
CURL *curl = curl_easy_init();
if(curl) {
printf("libcurl version: %s\n", curl_version());
curl_easy_cleanup(curl);
}
return 0;
}
✅ Используем #include "filename" для включения заголовочных файлов своего проекта. Препроцессор будет сначала искать эти файлы там, где вы ему об этом явно укажите с помощью опций.
// include/mylib.hpp - объявляем функцию из нашего проекта
#pragma once
void print_hello();
// src/main.cpp - используем локальный заголовочник через " "
#include <iostream> // Системный заголовочник
#include "mylib.hpp" // Заголовочник локального проекта, ищем в указанных путях
int main() {
print_hello();
return 0;
}
void print_hello() {
std::cout << "Hello from my project!\n";
}
// компиляция: g++ -Iinclude/ src/main.cpp -o my_program -std=c++17
See the difference. Stay cool.
#cppcore #compiler
1❤61👍25🔥9
Последний элемент enum
#новичкам
С enum'ами в С++ можно творить разное-безобразное. Можно легко конвертить элементы enum'а в числа и инициализировать их числом. Мы в это сейчас глубоко не будем погружаться, а возьмем базовый сценарий использования. Вам дано перечисление:
И в каком-то месте программы вам нужно узнать размер этого перечисления. Вопрос: как в коде получить его размер?
В таком варианте, когда элементам enum'а явно не присвоены никакие числа, каждому из них присвоен порядковый номер, начиная с нуля. kRed - 0, kGreen - 1, kBlue - 2.
Соответственно, чтобы получить количество элементов перечисления нужно сделать такую операцию:
Это работает, но выглядит что-то не очень. Читающий этот код конечно догадывается, что если мы хотим получить размер, то kBlue должен быть последним элементом. Но это вообще никем не гарантируется. Особенно, если в какой-то момент цветов станет больше:
И все. Код получения размера поломался. И надо везде его исправлять теперь. В общем, подход не расширяемый и требует модификации большого количество кода.
На этот случай есть проверенный прием: заранее вставлять в enum фейковый последний элемент, порядковый номер которого и будет равен размеру перечисления:
В этом случае расширять enum нужно приписывая элементы перед kCount. А код получения размера не меняется.
Эта фишка повсеместно используется в реальных проектах, поэтому новичкам полезно будет это знать.
Create extendable solutions. Stay cool.
#goodpractice #cppcore
#новичкам
С enum'ами в С++ можно творить разное-безобразное. Можно легко конвертить элементы enum'а в числа и инициализировать их числом. Мы в это сейчас глубоко не будем погружаться, а возьмем базовый сценарий использования. Вам дано перечисление:
enum class Color {
kRed,
kGreen,
kBlue
};И в каком-то месте программы вам нужно узнать размер этого перечисления. Вопрос: как в коде получить его размер?
В таком варианте, когда элементам enum'а явно не присвоены никакие числа, каждому из них присвоен порядковый номер, начиная с нуля. kRed - 0, kGreen - 1, kBlue - 2.
Соответственно, чтобы получить количество элементов перечисления нужно сделать такую операцию:
auto size = static_cast<int>(Color::kBlue) + 1;
Это работает, но выглядит что-то не очень. Читающий этот код конечно догадывается, что если мы хотим получить размер, то kBlue должен быть последним элементом. Но это вообще никем не гарантируется. Особенно, если в какой-то момент цветов станет больше:
enum class Color {
kRed,
kGreen,
kBlue,
kBlack
};И все. Код получения размера поломался. И надо везде его исправлять теперь. В общем, подход не расширяемый и требует модификации большого количество кода.
На этот случай есть проверенный прием: заранее вставлять в enum фейковый последний элемент, порядковый номер которого и будет равен размеру перечисления:
enum class Color {
kRed,
kGreen,
kBlue,
kCount
};
auto size = static_cast<int>(Color::kCount);В этом случае расширять enum нужно приписывая элементы перед kCount. А код получения размера не меняется.
Эта фишка повсеместно используется в реальных проектах, поэтому новичкам полезно будет это знать.
Create extendable solutions. Stay cool.
#goodpractice #cppcore
❤69👍20🔥14
Cпецификатор, модификатор, квалификатор и идентификатор
#новичкам
Когда люди учат иностранные языки, то после определенного уровня они начинают изучать идиомы языка, чтобы звучать как нейтив спикеры.
При описании С++ кода тоже можно пользовать определенные словечки, чтобы все понимали, что ты "про". Среди них выделяются
Начнем с простого. Идентификатор. Это просто имя, которым "идентифицируется" сущность. Имя переменной, константы, функции, класса, шаблона - это идентификаторы. Такие себе id'шники сущностей.
Спецификатор. Это слово скрывает в себе самое большое разнообразие сущностей. В основном это ключевые слова, уточняющие, что это за сущность:
- Спецификаторы типа. Ключевые слова, использующиеся для определения типа или сущности. class и struct(при объявлении класса указываем что идентификатор является классом), enum, все тривиальные типы(char, bool, short, int, long, signed, unsigned, float, double, void), объявленный прежде имена классов, enum'ов, typedef'ов.
- Спецификаторы объявления. typedef, inline, friend, conetexpr, consteval, constinit, static, mutable, extern, thread_local, final, override.
- Спецификаторы доступа к полям классов: private, protected, public.
Модификатор
Модификатор типа - это ключевое слово, которое изменяет поведение стандартных числовых типов. Модификаторами являются: short, insigned, signed, long. Например, unsigned int - это уже беззнаковый тип, в short int - короткий тип инт, который обычно занимает 16 бит вместо 32.
Это слово редко используется, потому что все модификаторы - это спецификаторы. Так что это вносит только путаницу.
Квалификатор
Существует всего 4 квалификатора. cv-квалификаторы: const и volatile. И ref-квалификаторы: & и &&.
Все. Теперь вы native говоруны и можете speak как про С++ coders.
Know the meaning. Stay cool.
#cppcore
#новичкам
Когда люди учат иностранные языки, то после определенного уровня они начинают изучать идиомы языка, чтобы звучать как нейтив спикеры.
При описании С++ кода тоже можно пользовать определенные словечки, чтобы все понимали, что ты "про". Среди них выделяются
спецификатор, модификатор, квалификатор и идентификатор. Они очень похожи и непонятно, в какой ситуации применять эти слова. Сегодня разрушим эту лингвистическую преграду к высотам артикуляции кода.Начнем с простого. Идентификатор. Это просто имя, которым "идентифицируется" сущность. Имя переменной, константы, функции, класса, шаблона - это идентификаторы. Такие себе id'шники сущностей.
Спецификатор. Это слово скрывает в себе самое большое разнообразие сущностей. В основном это ключевые слова, уточняющие, что это за сущность:
- Спецификаторы типа. Ключевые слова, использующиеся для определения типа или сущности. class и struct(при объявлении класса указываем что идентификатор является классом), enum, все тривиальные типы(char, bool, short, int, long, signed, unsigned, float, double, void), объявленный прежде имена классов, enum'ов, typedef'ов.
- Спецификаторы объявления. typedef, inline, friend, conetexpr, consteval, constinit, static, mutable, extern, thread_local, final, override.
- Спецификаторы доступа к полям классов: private, protected, public.
Модификатор
Модификатор типа - это ключевое слово, которое изменяет поведение стандартных числовых типов. Модификаторами являются: short, insigned, signed, long. Например, unsigned int - это уже беззнаковый тип, в short int - короткий тип инт, который обычно занимает 16 бит вместо 32.
Это слово редко используется, потому что все модификаторы - это спецификаторы. Так что это вносит только путаницу.
Квалификатор
Существует всего 4 квалификатора. cv-квалификаторы: const и volatile. И ref-квалификаторы: & и &&.
Все. Теперь вы native говоруны и можете speak как про С++ coders.
Know the meaning. Stay cool.
#cppcore
🔥49👍16❤12🤓6
Как итерироваться в обратном порядке?
#новичкам
Кто часто решал задачки на литкоде поймут проблему. Есть вектор и надо проитерироваться по нему с конца. Ну пишем:
В чем проблема этого кода?
Бесконечный цикл и ub. auto определяет тип i беззнаковым, который физически не может быть меньше нуля. Происходит переполнение, i становится очень большим и происходит доступ к невалидной памяти.
В большинстве задач можно написать тип int и все будет работать. Но все-таки size() возвращает size_t и будет происходить сужающее преобразование. В реальных проектах нужно избегать этого и сегодня мы посмотрим, как безопасно итерироваться в обратном порядке.
✅ Использовать свободную функцию ssize() из C++20:
Ее можно применить к вектору и она вернет значение типа std::ptrdiff_t. В первом приблежении это знаковый аналог std::size_t, который позволяет вычислять расстояние между двумя указателями, даже для очень больших массивов.
Так как тип знаковый и в большинстве реализаций его размер сопоставим с size_t, то можно не переживать по поводу возможной срезки длины вектора до меньшего типа.
✅ Использовать обратные итераторы:
Тут все довольно очевидно и безопасно.
Однако cppcore гайдлайны говорят нам, что нужно предпочитать использовать range-based-for циклы обычным for'ам. Чтож, давайте пойдем в эту сторону.
✅ Написать свой легковесный адаптер для итерирования в обратном порядке:
Делаем тонкую обертку над любым итерируемым объектом(в рабочем коде нужно всяких концептов навесить, чтобы было прям по-красоте) и элегантно итерируемся по контейнеру.
✅ А ренджи для кого придумали? Они для этой задачи подходят идеально:
Рэнджи из C++20 предоставляют кучу удобных адаптеров для работы с контейнерами. В сущности std::views::reverse или std::ranges::reverse_view делает примерно то же самое, что и мы сами написали в третьем пункте.
Можно совсем упороться и применить алгоритмы ренждей:
Бывает, что индексы элементов все-таки нужны внутри цикла. Но это решается с помощью std::ranges::iota_view. Оставляем реализацию этого решения для домашних изысканий.
Have a large toolkit. Stay cool.
#cppcore #cpp20 #STL
#новичкам
Кто часто решал задачки на литкоде поймут проблему. Есть вектор и надо проитерироваться по нему с конца. Ну пишем:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = vec.size() - 1; i >= 0; --i) {
std::cout << i << ": " << vec[i] << '\n';
}В чем проблема этого кода?
Бесконечный цикл и ub. auto определяет тип i беззнаковым, который физически не может быть меньше нуля. Происходит переполнение, i становится очень большим и происходит доступ к невалидной памяти.
В большинстве задач можно написать тип int и все будет работать. Но все-таки size() возвращает size_t и будет происходить сужающее преобразование. В реальных проектах нужно избегать этого и сегодня мы посмотрим, как безопасно итерироваться в обратном порядке.
✅ Использовать свободную функцию ssize() из C++20:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = std::ssize(vec) - 1; i >= 0; --i) {
std::cout << vec[i] << '\n';
}Ее можно применить к вектору и она вернет значение типа std::ptrdiff_t. В первом приблежении это знаковый аналог std::size_t, который позволяет вычислять расстояние между двумя указателями, даже для очень больших массивов.
Так как тип знаковый и в большинстве реализаций его размер сопоставим с size_t, то можно не переживать по поводу возможной срезки длины вектора до меньшего типа.
✅ Использовать обратные итераторы:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto it = std::rbegin(vec); it != std::rend(vec); ++it)
std::cout << *it << '\n';Тут все довольно очевидно и безопасно.
Однако cppcore гайдлайны говорят нам, что нужно предпочитать использовать range-based-for циклы обычным for'ам. Чтож, давайте пойдем в эту сторону.
✅ Написать свой легковесный адаптер для итерирования в обратном порядке:
template <typename T>
class reverse {
private:
T &iterable_;
public:
explicit reverse(T &iterable) : iterable_{iterable} {}
auto begin() const { return std::rbegin(iterable_); }
auto end() const { return std::rend(iterable_); }
};
std::vector vec{1, 2, 3, 4, 5};
for (const auto &elem : reverse(vec))
std::cout << elem << '\n';
Делаем тонкую обертку над любым итерируемым объектом(в рабочем коде нужно всяких концептов навесить, чтобы было прям по-красоте) и элегантно итерируемся по контейнеру.
✅ А ренджи для кого придумали? Они для этой задачи подходят идеально:
for (const auto& elem : vec | std::views::reverse)
std::cout << elem << '\n';
// или без пайпов
for (const auto& elem : std::ranges::reverse_view(vec))
std::cout << elem << '\n';
Рэнджи из C++20 предоставляют кучу удобных адаптеров для работы с контейнерами. В сущности std::views::reverse или std::ranges::reverse_view делает примерно то же самое, что и мы сами написали в третьем пункте.
Можно совсем упороться и применить алгоритмы ренждей:
std::ranges::copy(vec | std::views::reverse,
std::ostream_iterator<int>( std::cout,"\n" ));
// или c лямбдой
std::ranges::for_each(vec | std::views::reverse,
[](const auto& elem) {
std::cout << elem << '\n';
});
Бывает, что индексы элементов все-таки нужны внутри цикла. Но это решается с помощью std::ranges::iota_view. Оставляем реализацию этого решения для домашних изысканий.
Have a large toolkit. Stay cool.
#cppcore #cpp20 #STL
❤36👍23🔥11👎3👀2❤🔥1
ssize_t
#новичкам
Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.
Зачем такой тип нужен?
В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:
Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:
Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.
Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.
Use the right tool. Stay cool.
#cppcore #goodoldc #NONSTANDARD
#новичкам
Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.
Зачем такой тип нужен?
В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void buf, size_t count);
Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) { / Обработка ошибки */ }
Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.
Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.
Use the right tool. Stay cool.
#cppcore #goodoldc #NONSTANDARD
🔥38👍19❤10😁3👎1
Преимущества std::make_shared
#новичкам
Попсовая тема, которая часто спрашивается на собеседованиях. Краем касались ее в других постах, но пусть будет и отдельно, для удобной пересылки.
Коротко о том, что это за функция.
Это по сути фабрика для создания шаред поинтеров из параметров конструктора разделяемого объекта. Внутри себя она производит аллокацию памяти и вызов конструктора с помощью переданных аргументов на этой памяти.
В чем же преимущества этой функции по сравнению с явным вызовом конструктора shared_ptr?
Ну для начала, она не предполагает явного использования сырых указателей. Никакого вызова new!
Сами по себе сырые указатели - это неплохо. Просто на душе спокойнее, когда их как можно меньше в современном С++ коде.
Но если new не вызывает программист, это не значит, что функция его не вызывает. Еще как вызывает. И в том, как она это делает кроется главное преимущество std::make_shared над явным вызовом конструктора.
ОООчень упрощенно внутреннее устройство std::shared_ptr выглядит вот так:
Это два указателя: на сам объект и на контрольный блок, в котором находятся счетчики ссылок и некоторая другая информация.
Память под объекты, на которые указывают эти указатели, обычно выделяется раздельно:
Память под объект Foo выделяется при вызове new, а память под контрольный блок выделяется внутри конструктора shared_ptr.
При явном вызове конструктора невозможно по-другому: будет две аллокации.
Но когда make_shared забирает у пользователя возможность самому вызывать конструктор, у нее появляется уникальная возможность: за один раз выделить один большой кусок памяти, в который влезет и объект, и контрольный блок:
Это очень упрощенная реализация, которая показывает главный принцип: выделяется один кусок памяти под два объекта.
Отсюда повышение производительности за счет уменьшения количества аллокаций и за счет большей локальности данных и кеш-френдли структурой.
Ну и на последок.
В этой записи два раза повторяется имя класса. В коде могут быть довольно длинные названия сущностей, даже при использовании алиасов. Получается в каком-то смысле явный вызов конструктора приводит к дублированию кода.
Это не происходит с std::make_shared, потому что у нас есть волшебное слово auto:
Есть(было) и еще одно преимущество make_shared. Но его разберем уже отдельно, там ситуация непростая.
А на этом у нас все)
Make better tools. Stay cool.
#cppcore #memory
#новичкам
Попсовая тема, которая часто спрашивается на собеседованиях. Краем касались ее в других постах, но пусть будет и отдельно, для удобной пересылки.
Коротко о том, что это за функция.
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
Это по сути фабрика для создания шаред поинтеров из параметров конструктора разделяемого объекта. Внутри себя она производит аллокацию памяти и вызов конструктора с помощью переданных аргументов на этой памяти.
В чем же преимущества этой функции по сравнению с явным вызовом конструктора shared_ptr?
Ну для начала, она не предполагает явного использования сырых указателей. Никакого вызова new!
Сами по себе сырые указатели - это неплохо. Просто на душе спокойнее, когда их как можно меньше в современном С++ коде.
Но если new не вызывает программист, это не значит, что функция его не вызывает. Еще как вызывает. И в том, как она это делает кроется главное преимущество std::make_shared над явным вызовом конструктора.
ОООчень упрощенно внутреннее устройство std::shared_ptr выглядит вот так:
template <typename T>
struct shared_ptr {
T * obj_ptr;
ControlBlock * block_ptr;
}
Это два указателя: на сам объект и на контрольный блок, в котором находятся счетчики ссылок и некоторая другая информация.
Память под объекты, на которые указывают эти указатели, обычно выделяется раздельно:
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
Память под объект Foo выделяется при вызове new, а память под контрольный блок выделяется внутри конструктора shared_ptr.
При явном вызове конструктора невозможно по-другому: будет две аллокации.
Но когда make_shared забирает у пользователя возможность самому вызывать конструктор, у нее появляется уникальная возможность: за один раз выделить один большой кусок памяти, в который влезет и объект, и контрольный блок:
template <typename T, typename... Args>
shared_ptr<T> my_make_shared(Args&&... args) {
// Выделяем память для ControlBlock и объекта T одним блоком
char* memory = new char[sizeof(ControlBlock) + sizeof(T)];
// Инициализируем ControlBlock в начале памяти
ControlBlock* block = new (memory) ControlBlock();
// Инициализируем объект T после ControlBlock
T* object = new (memory + sizeof(ControlBlock)) T(std::forward<Args>(args)...); // Placement new
shared_ptr<T> ptr;
ptr.obj_ptr = object;
ptr.block_ptr = block;
return ptr;
Это очень упрощенная реализация, которая показывает главный принцип: выделяется один кусок памяти под два объекта.
Отсюда повышение производительности за счет уменьшения количества аллокаций и за счет большей локальности данных и кеш-френдли структурой.
Ну и на последок.
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
В этой записи два раза повторяется имя класса. В коде могут быть довольно длинные названия сущностей, даже при использовании алиасов. Получается в каком-то смысле явный вызов конструктора приводит к дублированию кода.
Это не происходит с std::make_shared, потому что у нас есть волшебное слово auto:
auto ptr = std::make_shared<Foo>(arg1, arg2);
Есть(было) и еще одно преимущество make_shared. Но его разберем уже отдельно, там ситуация непростая.
А на этом у нас все)
Make better tools. Stay cool.
#cppcore #memory
👍40❤16🔥7💯2❤🔥1
Недостатки std::make_shared. Кастомный new и delete
#новичкам
В этой небольшой серии будем рассказывать уже о различных ограничениях при работе с std::make_shared.
И начнем с непопулярного.
Внутри себя она создает объект с помощью ::new. Это значит, что если вы для своего класса переопределяете операторы работы с памятью, то make_shared не будет учитывать это поведение, а вы будете гадать, почему не видите нужных спецэффектов:
В общем, если нужный кастомный менеджент памяти, то std::make_shared - не ваш бро.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
#новичкам
В этой небольшой серии будем рассказывать уже о различных ограничениях при работе с std::make_shared.
И начнем с непопулярного.
Внутри себя она создает объект с помощью ::new. Это значит, что если вы для своего класса переопределяете операторы работы с памятью, то make_shared не будет учитывать это поведение, а вы будете гадать, почему не видите нужных спецэффектов:
class A {
public:
void *operator new(size_t) {
std::cout << "allocate\n";
return ::new A();
}
void operator delete(void *a) {
std::cout << "deallocate\n";
::delete static_cast<A *>(a);
}
};
int main() {
const auto a =
std::make_shared<A>(); // ignores overloads
//const auto b =
// std::shared_ptr<A>(new A); // uses overloads
}
// OUTPUT:
// Пусто!В общем, если нужный кастомный менеджент памяти, то std::make_shared - не ваш бро.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
6❤28👍18😁14🔥4❤🔥1
Недостатки std::make_shared. Непубличные конструкторы
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
#cppcore #cpp11
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
#cppcore #cpp11
6❤14👍14🔥6⚡1
Недостатки std::make_shared. Кастомные делитеры
#новичкам
Заходим на cppreference и видим там такие слова:
Также видим ее сигнатуру:
И понимаем, что make_shared не предоставляет возможности указывать кастомный делитер. Все аргументы функции просто перенаправляются в конструктор шареного указателя.
Опустим рассуждения об оправданных кейсах применения кастомных делитеров для шареных указателей. Можете рассказать о своих примерах из практики в комментариях.
Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".
Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.
Получается и деаллокация для этих смежных частей одного отрезка памяти должна быть совместная, единая и через базовый оператор delete.
Если бы мы как-то хотели бы встроить делитер в эту схему, получился бы конфликт: делитер хочет удалить только объект, но им придется пользоваться и для освобождения памяти под контрольный блок. Это просто некорректное поведение.
Да и скорее всего, если вас устраивает помещать объект в шареный указатель вызовом дефолтного new, то устроит и использование дефолтного delete. Поэтому эта проблема тесно связана с проблемой из первой части серии, но не добавляет особых проблем сверх этого.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
#новичкам
Заходим на cppreference и видим там такие слова:
This function may be used as an alternative to std::shared_ptr<T>(new T(args...)).Также видим ее сигнатуру:
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
И понимаем, что make_shared не предоставляет возможности указывать кастомный делитер. Все аргументы функции просто перенаправляются в конструктор шареного указателя.
Опустим рассуждения об оправданных кейсах применения кастомных делитеров для шареных указателей. Можете рассказать о своих примерах из практики в комментариях.
Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".
Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.
Получается и деаллокация для этих смежных частей одного отрезка памяти должна быть совместная, единая и через базовый оператор delete.
Если бы мы как-то хотели бы встроить делитер в эту схему, получился бы конфликт: делитер хочет удалить только объект, но им придется пользоваться и для освобождения памяти под контрольный блок. Это просто некорректное поведение.
Да и скорее всего, если вас устраивает помещать объект в шареный указатель вызовом дефолтного new, то устроит и использование дефолтного delete. Поэтому эта проблема тесно связана с проблемой из первой части серии, но не добавляет особых проблем сверх этого.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
🔥17❤7👍6⚡2❤🔥1
Одно значимое улучшение С++17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
В каком порядке вызываются expr1, expr2, expr3, g, h и f?
Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.
Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.
Что было до С++17?
Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!
То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.
Полный бардак! И это приводило на самом деле к неприятным последствиям.
Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.
Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен.
Теперь такой код не будет проблемой:
потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
f( g(expr1, expr2), h(expr3) );
В каком порядке вызываются expr1, expr2, expr3, g, h и f?
Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.
Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.
Что было до С++17?
Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!
То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.
Полный бардак! И это приводило на самом деле к неприятным последствиям.
Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
new SomeClass1{} -> new SomeClass2{} -> std::unique_ptr<SomeClass1> -> std::unique_ptr<SomeClass2>Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
bar(std::make_unique<SomeClass1>(), std::make_unique<SomeClass2>());
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
f(e(), g(expr1, expr2), h(expr3));
До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.
Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен.
Теперь такой код не будет проблемой:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp17
👍36❤15🔥10❤🔥4
Помогите Доре найти ошибку
#опытным
А у нас новая рубрика #бага, где мы пытаемся найти нетривиальные ошибки в коде. Коллективные усилия и жаркие обсуждения в комментариях приветствуются.
Вот такой код:
Это доморощенная и обрезанная версия вектора. Понятное дело, что здесь многого не хватает и этим нельзя пользоваться. Но зато это уже компилируется.
Тем не менее даже в таком маленьком кусочке кода есть принципиальная бага.
Сможете найти? Пишите свои варианты в комментариях.
Правильный ответ с пояснениями и фиксом будет завтра.
Deduce the error. Stay cool.
#опытным
А у нас новая рубрика #бага, где мы пытаемся найти нетривиальные ошибки в коде. Коллективные усилия и жаркие обсуждения в комментариях приветствуются.
Вот такой код:
template <typename T>
class Vector {
private:
T *m_data;
T *m_endSize;
T *m_endCapacity;
public:
// Use a different type "U to support const and non-const
template <typename U>
class Iterator {
private:
U *m_ptr;
public:
Iterator(U *ptr) : m_ptr{ptr} {}
U &operator*() const { return *m_ptr; }
};
template <typename Self>
auto begin(this Self &&self) {
return Iterator(self.m_data);
}
};
Это доморощенная и обрезанная версия вектора. Понятное дело, что здесь многого не хватает и этим нельзя пользоваться. Но зато это уже компилируется.
Тем не менее даже в таком маленьком кусочке кода есть принципиальная бага.
Сможете найти? Пишите свои варианты в комментариях.
Правильный ответ с пояснениями и фиксом будет завтра.
Deduce the error. Stay cool.
❤13👍7🤓6🔥3🤔1
Бага обнаружена!
#опытным
Проблема в текущей реализации
заключается в том, что константность не правильно распространяется через итератор при работе с const-объектами Vector. Давайте по порядку
Когда мы имеем
В текущей реализации, когда вызывается
Шаблонный вывод типов в форме CTAD отбрасывает верхнеуровневую константность: для
В результате мы получаем итератор, который позволяет изменять элементы, даже когда Vector константный.
То есть, такой код становится вполне валиден:
Что не есть хорошо.
Выходом тут будет не использовать CTAD, а явно и гибко задавать тип шаблонного параметра на основе константности self и нескольких трейтов:
По пути еще чуть-чуть причесываем передачу универсальной ссылки через std::forward.
Fix the problem. Stay cool.
#template #cppcore
#опытным
Проблема в текущей реализации
template <typename T>
class Vector {
private:
T *m_data;
T *m_endSize;
T *m_endCapacity;
public:
// Use a different type "U to support const and non-const
template <typename U>
class Iterator {
private:
U *m_ptr;
public:
Iterator(U *ptr) : m_ptr{ptr} {}
U &operator*() const { return m_ptr; }
};
template <typename Self>
auto begin(this Self &&self) {
return Iterator(self.m_data);
}
};
заключается в том, что константность не правильно распространяется через итератор при работе с const-объектами Vector. Давайте по порядку
Когда мы имеем
const Vector<T>, поле m_data становится T* const (константный указатель на T), а не const T* (указатель на константный T). Так называемая синтаксическая или поверхностная константность.В текущей реализации, когда вызывается
begin() на const-объекте:Self выводится как const Vector<T>& self.m_data имеет тип T const Шаблонный вывод типов в форме CTAD отбрасывает верхнеуровневую константность: для
Iterator создает Iterator<T>, а не Iterator<const T> В результате мы получаем итератор, который позволяет изменять элементы, даже когда Vector константный.
То есть, такой код становится вполне валиден:
const Vector<int> vec{1, 2, 3};
auto it = vec.begin();
*it = 2;Что не есть хорошо.
Выходом тут будет не использовать CTAD, а явно и гибко задавать тип шаблонного параметра на основе константности self и нескольких трейтов:
template<typename Self>
auto begin(this Self&& self)
{
using value_type = std::conditional_t<
std::is_const_v<std::remove_reference_t<Self>>,
const T,
T>;
return Iterator<value_type>(std::forward<Self>(self).m_data);
}
По пути еще чуть-чуть причесываем передачу универсальной ссылки через std::forward.
Fix the problem. Stay cool.
#template #cppcore
1👍20❤12🔥5❤🔥3
Как использовать std::unordered_map с ключом в виде std::pair?
#опытным
При работе над задачами C++ часто необходимо использовать сложные ключи в контейнерах на основе хэша - std::unordered_map. Распространенным подходом является использование std::pair<int, int> в качестве типа ключа. Однако попытка объявить unordered_map следующим образом:
приводит к подобной ошибке компиляции:
Происходит это, потому что для std::pair не определена хэш-функция. Она нужна для превращения значение объекта-ключа в число, которое используется для индексации элемента в хэш-таблице.
STL предоставляет нам хэш-функции для тривиальных типов данных и, например, std::string.
Но для сложных шаблонных типов непонятно в общем случае, как реализовать хэш-функцию. Поэтому эту задачу и возложили на самих программистов. Нужно самим определять хэш-функцию для объекта так, как того требует конкретная задача.
Ну хорошо. Определять надо. Но как это сделать? В азбуке не написано, как написать хэш для пары...
Давайте по порядку. Самый тривиальный подход - просто ксорим два хэша типов пары(в предположении, что они уже есть):
Отлично, заработало! Или нет?
Это компилируется, но есть проблема с коллизиями. Если ключом будет std::pair<int, int>, то для двух разных ключей {1, 2} и {2, 1} будут одинаковые хэши. Не очень хорошо.
Сделаем ход конем:
Побитово сдвинем второй хэш на один бит влево. Так мы не сильно ухудшим распределение(всего один бит заменим на нолик), но уберем коллизии.
Но это конечно все на коленке сделаный велосипед и можно найти антипримеры. В бусте есть функция hash_combine, которая делает ровно то, что мы хотим:
Если хочется узнать, что там у этой штуки под капотом, что в сущности код выше будет эквивалентен следующему коду:
Магические числа во всей красе. Но это нормально, когда мы имеем дело с математикой: генераторы случайных чисел, шифрование, хэш-функции.
Кстати, естественно, что такой подход можно использовать и для кастомных структур, и для туплов. В общем, можно пользоваться. Хотите тяните буст, хотите сами пишите, там все равно не так сложно.
Use ready-made solutions. Stay cool.
#cppcore #STL #template
#опытным
При работе над задачами C++ часто необходимо использовать сложные ключи в контейнерах на основе хэша - std::unordered_map. Распространенным подходом является использование std::pair<int, int> в качестве типа ключа. Однако попытка объявить unordered_map следующим образом:
std::unordered_map<std::pair<int, int>, int> map;
приводит к подобной ошибке компиляции:
error: call to implicitly-deleted default constructor of
'unordered_map<std::pair<int, int>, int>'
Происходит это, потому что для std::pair не определена хэш-функция. Она нужна для превращения значение объекта-ключа в число, которое используется для индексации элемента в хэш-таблице.
STL предоставляет нам хэш-функции для тривиальных типов данных и, например, std::string.
Но для сложных шаблонных типов непонятно в общем случае, как реализовать хэш-функцию. Поэтому эту задачу и возложили на самих программистов. Нужно самим определять хэш-функцию для объекта так, как того требует конкретная задача.
Ну хорошо. Определять надо. Но как это сделать? В азбуке не написано, как написать хэш для пары...
Давайте по порядку. Самый тривиальный подход - просто ксорим два хэша типов пары(в предположении, что они уже есть):
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ h2;
}
};
}
std::unordered_map<std::pair<int, std::string>, double> map;
map[{42, "foo"}] = 3.14;Отлично, заработало! Или нет?
Это компилируется, но есть проблема с коллизиями. Если ключом будет std::pair<int, int>, то для двух разных ключей {1, 2} и {2, 1} будут одинаковые хэши. Не очень хорошо.
Сделаем ход конем:
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ (h2 << 1);
}
};
}
std::unordered_map<std::pair<int, int, double> map;Побитово сдвинем второй хэш на один бит влево. Так мы не сильно ухудшим распределение(всего один бит заменим на нолик), но уберем коллизии.
Но это конечно все на коленке сделаный велосипед и можно найти антипримеры. В бусте есть функция hash_combine, которая делает ровно то, что мы хотим:
namespace std {
template <typename T1, typename T2>
struct hash<std::pair<T1, T2>> {
size_t operator()(const std::pair<T1, T2>& p) const {
size_t seed = 0;
boost::hash_combine(seed, p.first);
boost::hash_combine(seed, p.second);
return seed;
}
};
}Если хочется узнать, что там у этой штуки под капотом, что в сущности код выше будет эквивалентен следующему коду:
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
}Магические числа во всей красе. Но это нормально, когда мы имеем дело с математикой: генераторы случайных чисел, шифрование, хэш-функции.
Кстати, естественно, что такой подход можно использовать и для кастомных структур, и для туплов. В общем, можно пользоваться. Хотите тяните буст, хотите сами пишите, там все равно не так сложно.
Use ready-made solutions. Stay cool.
#cppcore #STL #template
❤37🔥15👍8😁7
Методы, определенные внутри класса
#новичкам
Вы хотите написать header-only библиотеку логирования и собственно пишите:
Ваши пользователи вызывают из одной единицы трансляции метод Log:
И из второй:
А потом это все успешно линкуется в один бинарник. Как так? Должно же было сработать One Definition Rule, которое запрещает иметь более одного определения функции на всю программу? А у нас как раз все единицы трансляции видят определение метода Log.
Дело в том, что все методы, определенные внутри тела класса, неявно помечены как inline. Это не значит, что компилятор встроит код этих методов в вызывающий код. Это значит, что для таких методов разрешается иметь сколько угодно одинаковых определений внутри программы. На этапе линковки выберется одна любая реализация и везде, где будет нужен адрес метода для вызова будет подставляться адрес именно этой реализации.
Так что явно использовать ключевое слово inline в этом случае бессмысленно.
Но и в обычном, не херед-онли коде, можно определять методы внутри класса. Когда это стоит делать?
Каждая единица трансляции должна сгенерировать свой код для inline метода. Это значит, что обильное использование inline методов может привести к увеличенному времени компиляции.
Однако наличие определения метода внутри класса может быть использовано компилятором для встраивания его кода в caller. Это снижает издержки на вызов метода.
Противоречивые последствия. Либо быстрый рантайм и медленный компайл-тайм, либо наоборот. Как быть?
Обычно inline делают простые и короткие методы, типа сеттеров и геттеров, а длинные методы, которые менее вероятно будут встраиваться, выносят в цпп. Короткие функции сильнее всего страдают от оверхеда на вызов, который может быть сравним с временем выполнения самой функции. Но они не засоряют собой интерфейс класса, хэдэр также легко и быстро читается. Вот такой компромисс.
Look for a compromise. Stay cool.
#cppcore #goodpractice
#новичкам
Вы хотите написать header-only библиотеку логирования и собственно пишите:
// logger.hpp
namespace SimpleLogger {
enum class Level { Debug, Info, Warning, Error };
class Logger {
public:
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
static Logger &GetInstance() {
static Logger instance;
return instance;
}
void SetMinLevel(Level level) {
m_minLevel = level;
}
void Log(Level level, const std::string &message) {
if (level < m_minLevel)
return;
auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::lock_guard lock{m_mutex};
std::cout << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] " << "["
<< levelToString(level) << "] " << message << std::endl;
}
private:
Logger() : m_minLevel(Level::Info) { Log(Level::Info, "Logger initialized"); }
Level m_minLevel;
std::mutex m_mutex;
std::string levelToString(Level level) {
switch (level) {
case Level::Debug: return "DEBUG";
case Level::Info: return "INFO";
case Level::Warning: return "WARN";
case Level::Error: return "ERROR";
default: return "UNKNOWN";
}
}
};
} // namespace SimpleLogger
Ваши пользователи вызывают из одной единицы трансляции метод Log:
...
#include <logger.hpp>
...
using namespace SimpleLogger;
Logger::GetInstance().Log(Level::Info, "Select recent items");
db->Execute("Select bla bla");
...
И из второй:
...
#include <logger.hpp>
...
using namespace SimpleLogger;
if (!result) {
Logger::GetInstance().Log(Level::ERROR, "Result is empty");
throw std::runtime_error("Result is empty");
}
...
А потом это все успешно линкуется в один бинарник. Как так? Должно же было сработать One Definition Rule, которое запрещает иметь более одного определения функции на всю программу? А у нас как раз все единицы трансляции видят определение метода Log.
Дело в том, что все методы, определенные внутри тела класса, неявно помечены как inline. Это не значит, что компилятор встроит код этих методов в вызывающий код. Это значит, что для таких методов разрешается иметь сколько угодно одинаковых определений внутри программы. На этапе линковки выберется одна любая реализация и везде, где будет нужен адрес метода для вызова будет подставляться адрес именно этой реализации.
Так что явно использовать ключевое слово inline в этом случае бессмысленно.
Но и в обычном, не херед-онли коде, можно определять методы внутри класса. Когда это стоит делать?
Каждая единица трансляции должна сгенерировать свой код для inline метода. Это значит, что обильное использование inline методов может привести к увеличенному времени компиляции.
Однако наличие определения метода внутри класса может быть использовано компилятором для встраивания его кода в caller. Это снижает издержки на вызов метода.
Противоречивые последствия. Либо быстрый рантайм и медленный компайл-тайм, либо наоборот. Как быть?
Обычно inline делают простые и короткие методы, типа сеттеров и геттеров, а длинные методы, которые менее вероятно будут встраиваться, выносят в цпп. Короткие функции сильнее всего страдают от оверхеда на вызов, который может быть сравним с временем выполнения самой функции. Но они не засоряют собой интерфейс класса, хэдэр также легко и быстро читается. Вот такой компромисс.
Look for a compromise. Stay cool.
#cppcore #goodpractice
👍25🔥12❤9
Forwarded from Карьера в Bell Integrator
Они про скорость и многозадачность: асинхронные сетевые фреймворки для C++ 🚀
Когда дело доходит до обработки тысяч запросов, в бой вступает асинхронное программирование. Здесь потоки не блокируются при сетевых запросах или работе с файлами — они продолжают выполнять другие задачи.
Сравнительный анализ популярных С++ фреймворков, работающих как раз по такой парадигме, уже в карточках. Сделали, кстати, совместно с каналом Грокаем C++: там много полезного для разработчиков си-плюс-плюс, так что заходите!
#BellintegratorTeam #советыBell
Когда дело доходит до обработки тысяч запросов, в бой вступает асинхронное программирование. Здесь потоки не блокируются при сетевых запросах или работе с файлами — они продолжают выполнять другие задачи.
Сравнительный анализ популярных С++ фреймворков, работающих как раз по такой парадигме, уже в карточках. Сделали, кстати, совместно с каналом Грокаем C++: там много полезного для разработчиков си-плюс-плюс, так что заходите!
#BellintegratorTeam #советыBell
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25❤9🔥9👎1