пятница, 31 марта 2017 г.

[prog.thoughts] Блеск и нищета C++

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<char1024> 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<char1024> 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++ в современных условиях -- это конкурентное преимущество. И очень даже немаленькое. Но не для всех.

Комментариев нет: