
- Главная
- Каталог
- Интернет технологии
- Грокаем C++
Грокаем C++
Авторский канал о программировании на С++ и базе computer science. Простым и легким слогом рассказываем про сложные концепции С++. Самая активная и вовлеченная аудитория в тематике.
Статистика канала
switch(status_code) {
case HTTPStatus::OK:
processSuccess();
break;
case HTTPStatus::BadRequest:
case HTTPStatus::Unauthorized:
case HTTPStatus::Forbidden:
case HTTPStatus::NotFound:
logClientError(status_code);
break;
case HTTPStatus::InternalServerError:
case HTTPStatus::BadGateway:
case HTTPStatus::ServiceUnavailable:
return logServerError(status_code);
default:
// ...
}{}
Теперь магических чисел нет.
Плюс к этому все кейсы должны быть известны на этапе компиляции. Динамически менять ничего не получится.
Эта особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).
Дальше идем.
Чем вообще может быть плох switch?
👉🏿 Его использование смешивает логику выбора и обработки. Это может быть нормой при небольшом количестве веток. Все находится рядышком, не нужно прыгать по коду, чтобы понять полную картину.
Но это совершенно точно становится проблемой, когда количество кейсов больше 10-20. Свитч целиком перестает помещаться на экран и вместо того, чтобы при чтении кода понимать логику работы кода, вы разбираетесь в том, какой обработчик для каждого кейса вызывается.
👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.
👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.
Из этих проблем можно сделать несколько выводов:
1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.
2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.
3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.
void dispatch(Editor &editor, Command cmd) {
switch (cmd.type) {
case Command::NewFile:
executeNewFile(editor);
return;
case Command::OpenFile:
executeOpenFile(editor);
return;
// ...
}
}
void processUserInput(Editor &editor, UserInput input) {
if (auto cmd = interpretInput(input)) {
dispatch(editor, *cmd);
}
}{}
Когда не надо применять switch?
🔞 Пользовательский тип под условием.
🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)
🔞 Если нужен какой-то дикий полиморфизм, когда обработчик полиморфно выбирается на основе значения кейса.
В этих случаях обычно используют статическую std::unordered_map, сопоставляя значения кейсов их обработчикам.
Use the right tool. Stay cool.
#cppcore #goodpractice #design
switch(status_code) {
case 200: // OK
processSuccess();
break;
// ...
case 400: // Bad Request
logClientError(status_code);
break;
// ...
case 500: // Service Unavailable
logServerError(status_code);
break;
// ...
default:
handleUnknownStatus(status_code);
break;
}{}
То есть 1 переменная - много вариантов развития событий.
Теперь погнали по особенностям:
👉🏿 Если у вас одинаково обрабатываются разные значения, то вы можете проваливаться в нижележащие кейсы, пока не дойдете до нужного обработчика и не брякнитесь(встретите break):
switch(status_code) {
case 200: // OK
processSuccess();
break;
case 400: // Bad Request
case 401: // Unauthorized
case 403: // Forbidden
case 404: // Not Found
logClientError(status_code);
break;
}{}
👉🏿 Не очень удобно в switch работать с диапазонами значений. Скорее всего выразительнее и безопаснее воспользоваться обычными условиями, если у вас обработчики соответствуют длинным диапазонам.
Существуют расширения компилятора, которые позволяют указывать в switch диапазон:
switch(x) {
case 0 ... 9: // GNU Extension
std::cout << "0-9\n";
break;
case 10 ... 19: // GNU Extension
std::cout << "10-19\n";
break;
}{}
Но это непереносимо и вообще не для этого розочка цвела. switch хорошо оптимизируется через jump table(вместо кучи if-else с линейной сложностью используется массив меток обработчиков, в котором за О(1) ищется нужный), а диапазоны значений в эту концепцию не вписываются.
👉🏿 В С++ очень органично вписаны объекты и пользовательские типы, наряду с тривиальными С-совместимыми типами. Поэтому может сложиться впечатление, что switch может, например, со строками работать. Но нет, он работает только с целочисленными типами, то есть с целыми числами и перечислениями(scoped и unscoped).
Если хотите выбирать обработчики на основе пользовательских типов, нужно использовать ассоциативные контейнеры:
using Handler = std::function<void()>;
std::unordered_map<std::string, Handler> handlers = {
{"start", &start},
{"stop", []{ stop(); }}
};
void execute(const std::string& cmd) {
if (auto it = handlers.find(cmd); it != handlers.end()) {
it->second();
}
}{}
👉🏿 Значения кейсов должны быть константными выражениями. То есть значениями, известными на этапе компиляции. Вы не можете выбрать обработчик, соотвествующий runtime значению.
При использовании enum'ов такой проблемы в принципе не возникает, но вот с int'ами да:
constexpr int getValue() {
return 10;
}
int main() {
constexpr int y = getValue();
int z = getValue();
int x = 10;
switch (x) {
case y: // OK: y - constant expression
std::cout << "x equals y\n";
break;
case z: // ERROR: z is runtime value
std::cout << "x equals z\n";
break;
default:
std::cout << "default\n";
}
return 0;
}{}
Вроде простая конструкция, но все равно есть нюансы.
Explore nuances. Stay cool.
#cppcore
int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};
for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}{}
Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:
one and two
three and four{}
Но у вашего компилятора на это другое мнение. Кланг, например, выводит:
one and three{}
WAT? А где 2 и 4? И почему вообще 1 элемент?
С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.
Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:
/->/{/<-/{"one", "two"}, {"three", "four"}/->/}/<-/{}
Конструируя вектор с помощью универсальной инициализации, вы уже внутри самых внешних скобок должны перечислять элементы.
Вот и получается, что строка выше парсится компилятором, как одна пара.
Тогда получается, что вью на строку можно создать с помощью {"one", "two"}?!?
Без проблем. Вот вам подходящий конструктор:
template< class It, class End >
constexpr basic_string_view( It first, End last );{}
У нас же строковые литералы неявно приводятся к указателям. Очень уж похоже на то, что мы хотим создать вью на непрерывный поток байтов. Компилятор именно это и предполагает. Жаль, что только:
The behavior is undefined if [first, last) is not a valid range{}
оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.
Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.
А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.
Пофиксить эту неприятную неожиданность можно либо убрав лишнюю пару скобок, либо явно сказав, где вы хотите видеть пары:
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{std::pair{"one", "two"}, std::pair{"three", "four"}}
};{}
Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного pair. Компилятор, следуя правилам инициализации из списка, развернул вложенный список и интерпретировал содержимое как два отдельных элемента для вектора. Можно убедиться тут. Спасибо @Shuomi за комментарий по поводу различного поведения при разных стандартах)
Avoid ambiguity. Stay cool.
#STL #cpp17 #cpp23
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};
for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}{}
throw 1;{}
Бросаем число. А что, какие-то проблемы?
Или вот так:
throw nullptr;
throw "This is the end!";
void panic() { std::cout << "PANIC!" << std::endl; }
throw static_cast<void(*)()>(panic); // Указатель на функцию{}
Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.
Самое уморительное, что можно кинуть даже лямбду. Ведь это всего лишь объект замыкания, ничего более:
throw []{std::cout << "Things are going really bad...\n"; };{}
Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.
Так что при споре с коллегами вы теперь можете бросаться в них всеми предметами, от стула до
class X {
public:
X() : private_(1) {}
template <class T>
void f(const T &t) {}
int Value() { return private_; }
private:
int private_;
};{}
Чтобы трюк сработал, в классе должен быть шаблонный метод.
Теперь следите за руками.
Стандарт говорит, что вы самые хамские-хамы, если пытаетесь получить доступ к непубличным членам и будете за это жестко наказаны. Они должны быть использованы только внутри методов класса.
Дак, мы и не против. Давайте просто впишем новый метод класса, где изменим приватное поле, как нам нужно. И для этого даже не нужно менять код класса. И ключ ко всему - шаблонный метод.
Мы можем вне класса специлизировать шаблон метода для работы с конкретным типом. Специализация шаблона метода - это такой же метод с такими же правами, он может получать доступ к непубличным полям.
И тогда класс будет себя вести именно так, как мы ему скажем. А скажем мы ему пару ласковых:
struct Y {};
template <>
void X::f(const Y &) {
private_ = 2;
}
int main() {
X x;
std::cout << x.Value() << std::endl; // prints 1
x.f(Y());
std::cout << x.Value() << std::endl; // prints 2
}{}
В специализированном методе мы изменяем приватное поле и для наглядности выводим значение приватного поля в консоль. Можете сами убедиться, что это работает.
Этот трюк был описан Гербом Саттером, поэтому и называется Sutter hack.
Однако с его помощью нельзя менять поведение стандартных объектов:
The behavior of a C++ program is undefined if it declares
- an explicit specialization of any member function of a standard library class template, or
- an explicit specialization of any member function template of a standard library class or class template, or
- an explicit or partial specialization of any member class template of a standard library class or class template, or
- a deduction guide for any standard library class template.{}
потому что явные специализации методов из STL приводят к ub.
В общем, интересно, как на стыке двух концепций - ООП и шаблонов - появляются такие интересные спецэффекты)
Hack the life. Stay cool.
#cppcore #template #fun
template <typename Derived>
class ClientBase {
protected:
int sock_ = -1;
sockaddr_in serv_addr_{};
std::string server_ip_;
uint16_t port_;
Derived& derived() { return static_cast<Derived&>(this); }
public:
void send_message(const std::string& message) {
ssize_t bytes_sent = derived().send_impl(message);
if (bytes_sent < 0) {
throw std::runtime_error("Send failed");
}
}
};
class TcpClient : public ClientBase<TcpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_STREAM;
ssize_t send_impl(const std::string& message) {
return send(sock_, message.c_str(), message.length(), 0);
}
};
class UdpClient : public ClientBase<UdpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_DGRAM;
ssize_t send_impl(const std::string& message) {
return sendto(sock_, message.c_str(), message.length(), 0,
reinterpret_cast<sockaddr>(&serv_addr_), sizeof(serv_addr_));
}
};{}
И меня всегда напрягало, что в любой статье вот эти *_impl методы находятся в публичном интерфейсе наследников. Зачем они там - непонятно и это по сути детали реализации. Надо бы их скрыть.
Но как? База CRTP должна иметь доступ к этим методам.
Вот тут-то мы и используем трюк. Сделаем все члены наследников приватными и дадим базе CRTP доступ к кишкам наследников через friend:
template <typename Derived>
class ClientBase {
// ...
};
class TcpClient : public ClientBase<TcpClient> {
private:
// ...
friend class ClientBase<TcpClient>;
};
class UdpClient : public ClientBase<UdpClient> {
private:
// ...
friend class ClientBase<UdpClient>;
};{}
И все. Теперь все члены приватные, ClientBase имеет ко всем из них доступ и публичный интерфейс задает только ClientBase, ничего лишнего нет.
Не так много народу знает об этой технике, поэтому решил о ней отдельно рассказать.
Hide your secrets. Stay cool.
#templateОтзывы канала
всего 7 отзывов
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
Грокаем C++ — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 9.3K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 13.7, количество отзывов – 7, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 8391.6 ₽, а за 30 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий