Третий аргумент main
#новичкам
Почти всегда вы пишите функцию main вот так:
Если вы пишите утилиту командной строки или просто хотите познать боль, то вам нужно парсить аргументы командной строки. В этом случае вы определяете main вот так:
Однако в стандарте описан еще один способ определения main:
Стандарт разрешает компиляторам давать возможность пользователям как-то по-другому задавать аргументы для main. И хоть это будет не переносимо, нам не всегда нужна кроссплатформенность.
Самая часто встречающаяся нестандартная сигнатура main следующая:
Третий аргумент main - это массив строк переменных окружения в формате "KEY=value". Массив завершается null pointer'ом.
Программа получает копию переменных окружения родительского процесса (например, терминала или скрипта). Только лишь независимую копию: изменение набора и значения переменных снаружи программы никак не влияет на то, что происходит внутри нее.
Вот минимальный примерчик:
Возможный вывод:
Вы не так часто можете увидеть этот формат сигнатуры main по уже очевидным для вас причинам:
- нестандарт
- а самое главное - это дело надо парсить. Засовывать в мапу какую-то и искать потом по ключу нужную переменную. А зачем, если есть std::getenv или его брат getenv из Сей.
Рассказываю про это, чтобы вы при встрече в таким форматом аргументов main не думали, что что-то базовое упустили при изучении плюсов. Ну или просто для расширения кругозора)
Expand your horizons. Stay cool.
#NONSTANDARD #goodoldc
#новичкам
Почти всегда вы пишите функцию main вот так:
int main() {
// some code
}Если вы пишите утилиту командной строки или просто хотите познать боль, то вам нужно парсить аргументы командной строки. В этом случае вы определяете main вот так:
int main (int argc, char* argv[]) {
// argc - количество переданных аргументов
// argv - массив из переданных аргументов
// some parsing
}Однако в стандарте описан еще один способ определения main:
int main(/ implementation-defined /) {body}Стандарт разрешает компиляторам давать возможность пользователям как-то по-другому задавать аргументы для main. И хоть это будет не переносимо, нам не всегда нужна кроссплатформенность.
Самая часто встречающаяся нестандартная сигнатура main следующая:
int main(int argc, char* argv[], char* envp[]) {}Третий аргумент main - это массив строк переменных окружения в формате "KEY=value". Массив завершается null pointer'ом.
Программа получает копию переменных окружения родительского процесса (например, терминала или скрипта). Только лишь независимую копию: изменение набора и значения переменных снаружи программы никак не влияет на то, что происходит внутри нее.
Вот минимальный примерчик:
#include <iostream>
int main(int argc, char* argv[], char* envp[]) {
std::cout << "Environment variables:\n";
for (char** env = envp; *env != nullptr; env++) {
std::cout << *env << "\n";
}
return 0;
}
Возможный вывод:
PATH=/usr/local/bin:/usr/bin:/bin
USER=grokaem_cpp
...
Вы не так часто можете увидеть этот формат сигнатуры main по уже очевидным для вас причинам:
- нестандарт
- а самое главное - это дело надо парсить. Засовывать в мапу какую-то и искать потом по ключу нужную переменную. А зачем, если есть std::getenv или его брат getenv из Сей.
Рассказываю про это, чтобы вы при встрече в таким форматом аргументов main не думали, что что-то базовое упустили при изучении плюсов. Ну или просто для расширения кругозора)
Expand your horizons. Stay cool.
#NONSTANDARD #goodoldc
👍52❤19🔥4😱4
environ
#опытным
В POSIX-совместимых системах (Linux, macOS, BSD) существует еще один альтернатива параметру
В сущности, у нее такой же функционал, что и у envp, только эту переменную видно из любой единицы трансляции, так как это глобальная переменная, имеющая внешнюю линковку.
В остальном, особенности работы такие же, как и у envp.
Но если мы уже зашли по колено в Posix, то там есть функция setenv, которая позволяет менять переменные окружения уже в ходе выполнения программы:
И эти изменения будут видеть все ранее оговоренные методы получения значений env переменных. Однако это все не тредсейф и нужна защита в виде мьютексов.
Не встречал кейсов изменения переменных окружения. Наверное, это можно использовать, как костыльный механизм общения между модулями программы. Если у кого есть достойные примеры применения setenv, черканите в комментах, буду благодарен.
Интересно, как много в С|С++ методов получения окружения процесса. Но рекомендуется использовать конечно стандартный вариант std::getenv.
Be visiable. Stay cool.
#NONSTANDARD
#опытным
В POSIX-совместимых системах (Linux, macOS, BSD) существует еще один альтернатива параметру
envp в функции main() и функции std::getenv() для получения значений переменных окружения. Это глобальная переменная extern char** environ, которая предоставляет прямой доступ ко всему окружению процесса.В сущности, у нее такой же функционал, что и у envp, только эту переменную видно из любой единицы трансляции, так как это глобальная переменная, имеющая внешнюю линковку.
#include <stdio.h>
extern char** environ;
void foo() {
for(char** env = environ; *env != NULL; env++) {
printf("%s\n", *env);
}
}
В остальном, особенности работы такие же, как и у envp.
Но если мы уже зашли по колено в Posix, то там есть функция setenv, которая позволяет менять переменные окружения уже в ходе выполнения программы:
#include <stdlib.h>
int setenv(const char *envname, const char *envval, int overwrite);
И эти изменения будут видеть все ранее оговоренные методы получения значений env переменных. Однако это все не тредсейф и нужна защита в виде мьютексов.
Не встречал кейсов изменения переменных окружения. Наверное, это можно использовать, как костыльный механизм общения между модулями программы. Если у кого есть достойные примеры применения setenv, черканите в комментах, буду благодарен.
Интересно, как много в С|С++ методов получения окружения процесса. Но рекомендуется использовать конечно стандартный вариант std::getenv.
Be visiable. Stay cool.
#NONSTANDARD
20❤17👍9🔥8
Ревью
#новичкам
Пролистывал намедни один тг канал по С++, его название начинается на Senior и заканчивается на С++ Developer. Там обычно постится очень "качественный контент" и мне на глаза попался код, который я бы хотел закинуть вам на тряпкозакидательство.
В рамках #ревью мы приводим кусок кода, а вы в комментариях прожариваете его до полного well done: говорите, что вам не нравится, и как это исправить. Комментатора с самым большим количеством отмеченных проблем упомянем в завтрашнем посте с разбором.
Вот такой код. Под оригинальным постом с этим кодом коллеги призвали руки рубить за такой код. Давайте сделаем так, чтобы он не вызывал испанского стыда, а только возвышенные чувства платонической любви к С++.
Раз, два, три, код в порядок приведи!
Critique your decisions. Stay cool.
#новичкам
Пролистывал намедни один тг канал по С++, его название начинается на Senior и заканчивается на С++ Developer. Там обычно постится очень "качественный контент" и мне на глаза попался код, который я бы хотел закинуть вам на тряпкозакидательство.
В рамках #ревью мы приводим кусок кода, а вы в комментариях прожариваете его до полного well done: говорите, что вам не нравится, и как это исправить. Комментатора с самым большим количеством отмеченных проблем упомянем в завтрашнем посте с разбором.
struct Message {
int data;
};
std::queue<Message *> msgQueue;
void sender() {
for (int i = 0; i < 20; ++i) {
Message *msg = new Message();
msg->data = i;
msgQueue.push(msg);
std::cout << "Sent: " << msg->data << std::endl;
}
}
void receiver() {
while (true) {
if (msgQueue.empty()) {
break;
}
Message *msg = msgQueue.front();
msgQueue.pop();
std::cout << "Received: " << msg->data << std::endl;
delete msg;
}
}
int main() {
std::thread t1(sender);
std::thread t2(receiver);
t1.join();
t2.join();
return 0;
}Вот такой код. Под оригинальным постом с этим кодом коллеги призвали руки рубить за такой код. Давайте сделаем так, чтобы он не вызывал испанского стыда, а только возвышенные чувства платонической любви к С++.
Раз, два, три, код в порядок приведи!
Critique your decisions. Stay cool.
1❤26😱9🔥5👍4👎2😁2
Разбор ревью
#новичкам
Большое спасибо всем участникам ревью, которые проявили активность под предыщущим постом. Всем и каждому посылаем лучи благодарности!
Было непросто выбрать самый эффективный по критике комментарий, потому что некоторые люди предлагали странные решения. В итоге, мы выбрали @seweeex и вот его коммент. Давайте похлопаем ему 👏👏👏.
Теперь к сути. В этом коде не так уж и много проблем, просто они жирные и явно бросаются в глаза.
Поехали разбирать.
🔞 Зачем-то в очереди хранятся сырые указатели. Смысла в этом особого нет, кроме как подложить себе свинью на будущее и поджечь жёпы комментаторов. Тут даже умные указатели не нужны, зачем дополнительные аллокации? В очереди можно хранить сами объекты и никаких проблем с менеджментом памяти не будет.
🔞 Использование сырых указателей приводит например к тому, что при вылете исключения из метода push, произойдет утечка памяти. Да, элементов мы закидываем в очередь немного, но концептуально проблема есть. Решается это опять же через хранение обычных объектов.
🔞 Слон в посудной лавке здесь - это конечно отсутствие синхронизации на очереди. Это в принципе ub и дальше не о чем говорить. Нужна не стандартная, а потокобезопасная очередь.
Очередь должна быть блокирующей, чтобы не тратить активно ресурс CPU на ожидание нового сообщения. Это решается с помощью кондвара.
🔞 Ресивер может зашедулиться раньше сендера, увидит пустую очередь и выйдет из цикла, не обработав ни одной задачи. Поэтому нужна система сигнализации: очередь должна ждать прихода новых задач, пока ей не скажут, что больше задач нет.
🔞 Бесконечный цикл в ресивере выглядит не очень. Если можно не писать бесконечных циклов и не вставлять брейки, то лучше этого не делать. break и continue усложняют понимание кода. Лучше перенести забирание элемента из очереди прям в шапку цикла.
🔞 Гонка на потоконебезопасном логировании. Нужен мьютекс, чтобы сообщения не интерферировали.
По сути все. Главное изменение - вынесение блокирующей потокобезопасной очереди в отдельный шаблонный класс, который хранит объекты типа Т. С шаблонами можно долго играться и далеко зайти, поэтому приведем самую простую реализацию, которая справляется со своими задачами в данном кейсе, но может быть улучшена для корректной работы с самыми разными типами:
Пишите свои дополнения, если что-то забыли.
Make things better. Stay cool.
#новичкам
Большое спасибо всем участникам ревью, которые проявили активность под предыщущим постом. Всем и каждому посылаем лучи благодарности!
Было непросто выбрать самый эффективный по критике комментарий, потому что некоторые люди предлагали странные решения. В итоге, мы выбрали @seweeex и вот его коммент. Давайте похлопаем ему 👏👏👏.
Теперь к сути. В этом коде не так уж и много проблем, просто они жирные и явно бросаются в глаза.
Поехали разбирать.
🔞 Зачем-то в очереди хранятся сырые указатели. Смысла в этом особого нет, кроме как подложить себе свинью на будущее и поджечь жёпы комментаторов. Тут даже умные указатели не нужны, зачем дополнительные аллокации? В очереди можно хранить сами объекты и никаких проблем с менеджментом памяти не будет.
🔞 Использование сырых указателей приводит например к тому, что при вылете исключения из метода push, произойдет утечка памяти. Да, элементов мы закидываем в очередь немного, но концептуально проблема есть. Решается это опять же через хранение обычных объектов.
🔞 Слон в посудной лавке здесь - это конечно отсутствие синхронизации на очереди. Это в принципе ub и дальше не о чем говорить. Нужна не стандартная, а потокобезопасная очередь.
Очередь должна быть блокирующей, чтобы не тратить активно ресурс CPU на ожидание нового сообщения. Это решается с помощью кондвара.
🔞 Ресивер может зашедулиться раньше сендера, увидит пустую очередь и выйдет из цикла, не обработав ни одной задачи. Поэтому нужна система сигнализации: очередь должна ждать прихода новых задач, пока ей не скажут, что больше задач нет.
🔞 Бесконечный цикл в ресивере выглядит не очень. Если можно не писать бесконечных циклов и не вставлять брейки, то лучше этого не делать. break и continue усложняют понимание кода. Лучше перенести забирание элемента из очереди прям в шапку цикла.
🔞 Гонка на потоконебезопасном логировании. Нужен мьютекс, чтобы сообщения не интерферировали.
По сути все. Главное изменение - вынесение блокирующей потокобезопасной очереди в отдельный шаблонный класс, который хранит объекты типа Т. С шаблонами можно долго играться и далеко зайти, поэтому приведем самую простую реализацию, которая справляется со своими задачами в данном кейсе, но может быть улучшена для корректной работы с самыми разными типами:
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <optional>
#include <print>
struct Message {
int data;
};
template<typename T>
class ThreadSafeQueue {
public:
void push(T msg) {
{
std::lock_guard lock(mutex_);
queue_.push(std::move(msg));
}
cv_.notify_one();
}
std::optional<T> pop() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || stopped_; });
if (stopped_ && queue_.empty()) {
return std::nullopt;
}
auto msg = std::move(queue_.front());
queue_.pop();
return msg;
}
void stop_receive() {
stopped_ = true;
cv_.notify_all();
}
private:
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::atomic<bool> stopped_ = false;
};
void println(const std::string& str) {
static std::mutex mtx;
std::lock_guard lock(mtx);
std::cout << str << std::endl;
}
void sender(ThreadSafeQueue<Message>& msgQueue) {
for (int i = 0; i < 20; ++i) {
auto msg = Message(i);
println("Sent: " + std::to_string(msg.data));
msgQueue.push(std::move(msg));
}
msgQueue.stop_receive();
}
void receiver(ThreadSafeQueue<Message>& msgQueue) {
while (auto msg = msgQueue.pop()) {
println("Received: " + std::to_string(msg.value().data));
}
}
int main() {
ThreadSafeQueue<Message> msgQueue;
std::thread t1(sender, std::ref(msgQueue));
std::thread t2(receiver, std::ref(msgQueue));
t1.join();
t2.join();
}
Пишите свои дополнения, если что-то забыли.
Make things better. Stay cool.
16❤34👍9👏5🤨1
or and not
#новичкам
В С/C++ всегда был не очень дружелюбный синтаксис операторов. Показать вот такой код человеку, который не в зуб ногой в программировании:
есть вероятность, что он подумает, что его прокляли шаманы тумба-юмба.
Однако знали ли вы, что в С/C++ есть альтернативный синтаксис токенов? Символы операторов заменяются на короткие слова и код выше становится почти питонячьим:
Выглядит свежо! Хотя было доступно еще с С++98.
Вот полный список альтернативных токенов:
Последние токены для скобок - это конечно дичь. Но остальные - вполне интересные варианты.
В сях альтернативные токены были введены в С95, поэтому до этого момента токенов не было в языке. Но даже с их введением все продолжали использовать привычный синтаксис. Видимо поэтому мы так до сих пор и остались на уровне наскальной живописи.
А вы используете в продакшен коде альтернативные токены?
Evolve. Stay cool.
#fun #goodoldc
#новичкам
В С/C++ всегда был не очень дружелюбный синтаксис операторов. Показать вот такой код человеку, который не в зуб ногой в программировании:
if (x > 0 && y < 10 || !z) {
// ...
}есть вероятность, что он подумает, что его прокляли шаманы тумба-юмба.
Однако знали ли вы, что в С/C++ есть альтернативный синтаксис токенов? Символы операторов заменяются на короткие слова и код выше становится почти питонячьим:
if (x > 0 and y < 10 or not z) {
// ...
}Выглядит свежо! Хотя было доступно еще с С++98.
Вот полный список альтернативных токенов:
&& - and
&= - and_eq
& - bitand
| - bitor
~ - compl
!= - not_eq
|| - or
|= - or_eq
^ - xor
^= - xor_eq
{ - <%
} - %>
[ - <:
] - :>
# - %:
## - %:%:
Последние токены для скобок - это конечно дичь. Но остальные - вполне интересные варианты.
В сях альтернативные токены были введены в С95, поэтому до этого момента токенов не было в языке. Но даже с их введением все продолжали использовать привычный синтаксис. Видимо поэтому мы так до сих пор и остались на уровне наскальной живописи.
А вы используете в продакшен коде альтернативные токены?
Evolve. Stay cool.
#fun #goodoldc
❤39👍11🔥7😱5👎3
#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