C++ -- это, блин, язык контрастов. В один день доводится увидеть совершенно крышесносящие примеры сочетания техники CRTP и variadic templates и наглядную демонстрацию того, как на C++ программируют в реальном мире.
Серьезно, если кто-то еще не читал эту небольшую PDF-у, в которой показываются трюки вроде:
template <typename N, template <typename...> class... CRTPs> class Number : public CRTPs<Number<N, CRTPs...>>... { public: using S = decay_t<underlying_arithmetic_type_t<N>>; constexpr Number() // note: intentionally uninitialized {} constexpr Number(S value) : value_(value) {} constexpr S value() const { return value_; } constexpr void set_value(S a) { value_ = a; } private: N value_; }; template <typename T> class Stream_i { friend std::ostream &operator <<(std::ostream &a, T b) { return a << b.value(); } }; template <typename T> class Shift_i { friend T operator <<(T a, T b) { return T(a.value() << b.value()); } friend T operator >>(T a, T b) { return T(a.value() >> b.value()); } }; template <typename T> class Eq_i { friend constexpr bool operator ==(T a, T b) { return a.value() == b.value(); } friend constexpr bool operator !=(T a, T b) { return a.value() != b.value(); } }; ... using restricted_int = Number<int, Eq_i, Rel_i, Add_i, Stream_i>; // Supports only ==, !=, <, >, <=, >=, +, +=, <<(ostream). |
то очень рекомендую. От нового взгляда на возможности современного C++ глаза распахиваются еще шире :)
Это, конечно же, с непривычки сносит крышу. Но, блин, ведь круто же. Очень интересный способ композиции возможностей на основе CRTP.
И на этом фоне реальный топик с LOR-а, в котором человек приводит свой код и просит подсказать, в чем проблема.
Код -- полный ахтунг. Если кому-то не жаль 15 минут времени, советую сходить на LOR, посмотреть полный текст. Под катом я попробую разобрать только его фрагмент.
Итак, фрагмент, который сразу же хочется переписать. Почему так я поясняю в комментариях к коду:
// Макросы зло! Вообще зло :) // // В C++ контанты лучше задавать типизированными константами. // Да и в конкрено этом случае отдельная константа вообще не потребуется. #define BUF_LEN 1024 // Мало того, что это макрос, так он еще и не безопасен по исключениям. // Если в action вылетит исключение, то mutex останется залоченным. #define WITH_MUTEX(mutex, action) {mutex.lock(); action; mutex.unlock();}; class server { private: bool opened = false; set<thread::id> client_threads; mutex client_mutex; int listener = -1; void client(int socket) { WITH_MUTEX(client_mutex, client_threads.insert(this_thread::get_id())); char buf[BUF_LEN]; int bytes_readed; // Обращаем внимание на тип int. string message; do { // На самом деле recv возвращает ssize_t. // Получится, что мы более "широкое" значение присваиваем // более "узкому", что не есть хорошо. Статические анализаторы // должны за это бить по рукам. bytes_readed = recv(socket, (void*)buf, BUF_LEN, 0); // А ведь bytes_readed не проверили на -1! int signal_on = -1; // Здесь не страшно, если bytes_readed == -1, цикл просто не // выполнится. for(int i=0; i<bytes_readed; ++i) { if(buf[i] == '\n') { signal_on = i; break; } } // Но вот здесь у нас начинаются проблемы. // Во-первых, если ничего не прочитали из сокета на текущей // итерации, то что будет находиться в buf? Вряд ли это нужно // добавлять в итоговый контейнер. // Во-вторых, даже если bytes_readed > 0, но в buf не был найден // перевод строки, то никто не гарантирует наличие в buf завершающего // 0-символа. Поэтому тут можно и мусор к message добавить, и даже // segmentation fault поймать. if(signal_on == -1) message += buf; else { if(signal_on != 0 && buf[signal_on - 1] == 13) signal_on--; // А это ручное копирование всех прочитанных байт до перевода строки. // Ручное, побайтовое копирование, Карл! for(int i=0; i<signal_on; ++i) message += buf[i]; bytes_readed = 0; } } while (bytes_readed > 0); cout << "message received: \"" << message << "\"" << endl; // Приведение от size_t к int. Не есть хорошо. // Даже на 32-х битовых платформах. sprintf(buf, "received %d len message\n", static_cast<int>(message.size())); send(socket, buf, strlen(buf), 0); // Закрываем сокет только если дошли сюда и не вылетели раньше // из-за какого-то исключения. close(socket); // Вычеркиваем ID текущей нити только если добрались сюда без исключений. WITH_MUTEX(client_mutex, client_threads.erase(this_thread::get_id())); } |
Принуждает ли C++ писать такой код? Нет, конечно.
Но проблема C++ в том, что он не мешает написать такой код. Более того, такой код будет даже работать большую часть времени. И, не исключено, практически в таком же стиле перерастет из quick-and-dirty примера во что-то более серьезное.
Честно говоря, не представлю, насколько нужно знать C++, чтобы писать в таком стиле. Такое впечатление, что тут речь идет вообще о незнании языка. Ну вот совсем.
Посему не дает мне покоя вопрос: неужели нужно иметь 20 лет опыта в C++, чтобы даже такой quick-and-dirty пример сразу записать ну хотя бы вот в таком виде:
// Простенький шаблон вместо макроса WITH_MUTEX. template<typename L> void with_mutex(mutex & mtx, L && action) { lock_guard<mutex> lock{mtx}; action(); } class server { private: bool opened = false; set<thread::id> client_threads; mutex client_mutex; int listener = -1; void client(int socket) { // Сразу готовимся к тому, чтобы сокет закрывался при любом выходе из функции. auto close_socket = cpp_util_3::at_scope_exit( [=]{ close(socket); }); // Добавляем ID текущей нити. with_mutex(client_mutex, [this]{ client_threads.insert(this_thread::get_id()); }); // И сразу же готовимся его изъять при любом выходе. auto remove_thread = cpp_util_3::at_scope_exit( [this]{ with_mutex(client_mutex, [this]{ client_threads.erase(this_thread::get_id()); }); }); array<char, 1024> buf; // Константа размера присутствует всего один раз. string message; bool should_continue = true; while(should_continue) { // Тип bytes_read выводится автоматически, нам не нужно об этом думать. const auto bytes_read = recv(socket, buf.data(), buf.size(), 0); if(bytes_read > 0) { const auto last = begin(buf) + bytes_read; // Используем стандартный find вместо ручного перебора в цикле. auto lf_pos = find(begin(buf), last, '\n'); if(last != lf_pos) { if(begin(buf) != lf_pos && '\r' == *(lf_pos-1)) --lf_pos; should_continue = false; } // Добавляем в итоговую строку либо все прочитанные символы, // либо до первого перевода строки. message.append(begin(buf), lf_pos); } else should_continue = false; } cout << "message received: \"" << message << "\"" << endl; // В стандартной библиотеке нет современной замены sprintf, // поэтому воспользуемся fmtlib. // При этом ответное сообщение, так же, как и в оригинальном коде, // запихнем все в тот же буффер buf, который использовался и для чтения. fmt::ArrayWritter fmtw(buf.data(), buf.size()); fmtw.write("received {} len message\n", message.size()); send(socket, fmtw.data(), fmtw.size(), 0); // Все, больше ничего делать не нужно, остальное автоматически // произойдет при выходе из функции. } |
Да, из-за скудности стандартной библиотеки C++ довелось воспользоваться двумя сторонними библиотеками: cpp_util (это наше поделие) и fmtlib. Но такой уж C++ сейчас, очень много чрезвычайно нужных в повседневной работе вещей находится в сторонних библиотеках, про которые нужно знать.
Хотя и этот вариант мне не очень нравится. Поскольку добавление thread::id во множество активных нитей и его изъятие оттуда -- это типичный образец для RAII. Ноу так и следует это именно в виде RAII и оформить. Пусть даже вспомогательного кода станет чуть больше:
template<typename L> void with_mutex(mutex & mtx, L && action) { lock_guard<mutex> lock{mtx}; action(); } class server { private: bool opened = false; set<thread::id> client_threads; mutex client_mutex; int listener = -1; friend class thread_id_sentinel { server & self_; public : thread_id_sentinel(const thread_id_sentinel &) = delete; thread_id_sentinel(thread_id_sentinel &&) = delete; thread_id_sentinel(server & self) : self_(self) { with_mutex(self_.client_mutex, [this]{ self_.client_threads.insert(this_thread::get_id()); }); } ~thread_id_sentinel() { with_mutex(self_.client_mutex, [this]{ self_.client_threads.erase(this_thread::get_id()); }); } }; void client(int socket) { auto close_socket = cpp_util_3::at_scope_exit( [=]{ close(socket); }); const auto thread_id_sentinel thread_id_guard{*this}; array<char, 1024> buf; string message; bool should_continue = true; while(should_continue) { const auto bytes_read = recv(socket, buf.data(), buf.size(), 0); if(bytes_read > 0) { const auto last = begin(buf) + bytes_read; auto lf_pos = find(begin(buf), last, '\n'); if(last != lf_pos) { if(begin(buf) != lf_pos && '\r' == *(lf_pos-1)) --lf_pos; should_continue = false; } message.append(begin(buf), lf_pos); } else should_continue = false; } cout << "message received: \"" << message << "\"" << endl; fmt::ArrayWritter fmtw(buf.data(), buf.size()); fmtw.write("received {} len message\n", message.size()); send(socket, fmtw.data(), fmtw.size(), 0); } |
Disclaimer: код ни разу не компилировался. Так что косяки обязательно должны быть.
Закрадываются темные мысли по поводу места C++ в современном мире. С одной стороны, C++ позволяет создавать шикарные библиотеки под конкретную задачу. С помощью которых эта самая конкретная задача решается просто и эффективно. Причем я не думаю, что для этого нужны какие-то запредельные усилия по изучению C++. Есть, конечно, сложные штуки, вроде CRTP на базе variadic templates. Но базовые-то вещи, вроде того же RAII и простейших шаблонов...
С другой стороны, в ИТ сейчас огромное количество людей. Без обид, но по сравнению с тем, что было лет 20 назад, программистов, удел которых писать на чем-то не сложнее Go и Python-а, сейчас просто невероятно много. И, определенно, к C++ их нельзя подпускать даже на пушечный выстрел. Что автоматически ведет к тому, что C++ реально будет нужен лишь очень ограниченному меньшинству разработчиков. Даже если C++ будет программировать пара миллионов во всем мире, это все равно будет капля в море по сравнению с количеством разработчиков на Java, JavaScript, Python, C#/VisualBasic, Go, Ruby и т.д.
Какие-то печальные перспективы для того, кто хотел бы зарабатывать на разработке инструментария для C++ :(
Хотя мне кажется, что C++ в современных условиях -- это конкурентное преимущество. И очень даже немаленькое. Но не для всех.
Комментариев нет:
Отправить комментарий