четверг, 22 июня 2023 г.

[prog.c++] Отличная статья для тех, кто хочет прокачать свое знание C++ных шаблонов

Вот: C++ template: Trying to Make the Easy Way. Букв много, но оно того стоит. Да к тому же там еще и ссылки на дополнительные материалы.

Совсем новичкам в C++ я бы ее рекомендовать не стал. Но если общие представления о шаблонах есть и хочется пойти дальше, то будет полезно. Да и бывалым C++никам имеет смысл ознакомиться. Я, например, для себя как минимум две вещи отметил:

Во-первых, вот такая форма SFINAE:

template <typename Object>
auto makeString(const Object& object) -> decltype(object.to_string())
{
    return object.to_string();
}

Я же, признаюсь, больше по старинке, через std::enable_if.

Во-вторых, необходимость применения std::forward в случаях, когда нужно вызвать какой-то метод у объекта, переданного посредством universal reference:

template <HasToString Object> // Это C++20, да. Не всем еще доступен.
std::string makeString(Object&& object)
{
    return std::forward<Object>(object).to_string();
}

В общем, мне понравилось. К прочтению рекомендую.

понедельник, 19 июня 2023 г.

[prog.c++] Давненько не приходилось видеть в C++ных проектах исключений, не производных от std::exception...

Сам я уже давно взял за правило, что если в проекте используются исключения, то собственные классы исключений в проекте должны быть прямо или косвенно унаследованы от std::exception. Прямо унаследованы -- это напрямую от std::exception. Косвенно -- это когда есть какой-то промежуточный класс-родитель, вроде std::runtime_error или some_3rd_party_lib::exception (при этом промежуточный класс-родитель все равно восходит к std::exception (прямо или косвенно)).

Следование этому правилу сильно упрощает жизнь. Т.к. можно просто писать:

try {
  ... // Какие-то действия.
}
catch( const std::exception & x ) {
  ... // Здесь мы точно знаем, что все исключения перехвачены.
}

Тогда как если у нас где-то есть выброс класса, не производного от std::exception (а то и вообще какого-нибудь int-а или std::string), то жизнь становится веселее и непредсказуемее.

Выделить можно два момента:

Во-первых, мы можем вообще не предполагать, что исключение какого-то левого типа XYZ у нас может вылететь. Хотя бы потому, что мы имеем дело со сторонней библиотекой X, в которой используется библиотека Y, в которой используется библиотека Z. И начиная с какой-то версии Z появился тип XYZ. Исключения этого типа не перехватили (возможно по недосмотру) в Y, и не перехватывали (понадеясь на Y) в X. Соответственно, мы снаружи X про XYZ вообще не сном, ни духом. И если в нашем коде обработка исключений сделана только через catch(const std::exception &), то у нас появляются серьезные проблемы, т.к. XYZ будет пролетать "насквозь" и, скорее всего, будет убивать наше приложение (или приложение того, кто использует наш код).

Во-вторых, даже если мы перестраховались и сделали в дополнение к catch(const std::exception &) еще и catch(...), то в этом перестраховочном catch мы мало что можем сделать. Даже толком не сможем залогировать тип пойманного исключения и полезную информацию из него.

В общем, сам давно придерживаюсь принципа, что все собственные исключения должны наследоваться от std::exception, и давным-давно не видел нарушений этого принципа в сторонних C++ных библиотеках, с которыми доводилось сталкиваться. Последний раз что-то подобное, ЕМНИП, видел в OTL лет 15 назад. Но там удалось убедить автора сделать специальные настроечные макросы OTL_EXCEPTION_DERIVED_FROM и OTL_EXCEPTION_IS_DERIVED_FROM_STD_EXCEPTION, чтобы otl_exception был наследником std::exception.

Но вот сегодня в дикой природе увидел исключения, не производные от std::exception. Например:

class EndOfStunMsgException {
public:
  EndOfStunMsgException() {}
  virtual ~EndOfStunMsgException() {}
};

Причем в копирайте там указаны даты 2011, 2012 и 2013 годы. Т.е. сильно после того, как появился стандарт C++98 (до этого формально std::exception в языке не было, так что каждый городил свою иерархию исключений по собственному разумению).

В связи с этим возникает вопрос: ну вот как так-то?

Нужно сказать, что там вообще к C++ному коду есть вопросы. Например, std::string используется, но специфическим образом:

  void addSTMessageIntegrity(std::string &uname, std::string &upwd) {

    if (!_constructed || !isCommand())
      throw WrongStunBufferFormatException();

    uint8_t *suname = (uint8_t *)strdup(uname.c_str());
    uint8_t *supwd = (uint8_t *)strdup(upwd.c_str());

    stun_attr_add_integrity_by_user_short_term_str(_buffer, &_sz, suname, supwd, SHATYPE_SHA1);

    free(suname);
    free(supwd);
  }

Метод принимает строки по неконстантной(!!!) ссылке, но содержимое параметров uname и upwd не меняется. Более-то, зачем-то делается копия их содержимого для вызова stun_attr_add_integrity_by_user_short_term_str. Временная копия, которая затем сразу же удаляется. Но если взглянуть внутрь stun_attr_add_integrity_by_user_short_term_str, то можно увидеть вот это:

int stun_attr_add_integrity_by_user_short_term_str(
    uint8_t *buf, size_t *len, const uint8_t *uname, password_t pwd, SHATYPE shatype) {
  if (stun_attr_add_str(buf, len, STUN_ATTRIBUTE_USERNAME, uname, (int)strlen((const char *)uname)) < 0)
    return -1;

  hmackey_t key;
  return stun_attr_add_integrity_str(TURN_CREDENTIALS_SHORT_TERM, buf, len, key, pwd, shatype);
}

Т.е. указатель на uint8_t все равно воспринимается как указатель на char (т.е. здесь нет никакой защиты от гипотетических случаев, когда почему-то char имеет другое представление, нежели uint8_t). Да еще и длина строки вычисляется через strlen, хотя изначально у нас есть std::string, длина которого известна через std::string::size().

И, кстати говоря, код возврата stun_attr_add_integrity_by_user_short_term_str в плюсовой addSTMessageIntegrity тупо игнорируется и теряется. Как и потенциальные ошибки strdup.

В общем, я уже давно не доверяю утверждениям "обычно C++ работает медленнее, чем чистый Си", потому что в достатке насмотрелся на случаи подобной пессимизации C++ного кода.


Сожалею, если этот пост получился излишне злым, но пригорело. Я уже несколько лет как безуспешно предлагаю свои услуги в качестве опытного C++разработчика. Такое ощущение, что это нафиг никому не нужно, поголовно у всех "свои крутые C++ специалисты". А как доведется в код заглянуть, так там и функции-простыни по 200 строк, и тотальное отсутствие комментариев, и коды возврата read не проверяются... Ну или вот такое, как показано выше. Ну OK, крутые так крутые.


Проект, фрагменты из которого были приведены в посте, мне использовать не нужно. Просто потребовалось получить представление о STUN, TURN, ICE и пр. Вот в процессе сбора информации и заглянул ненароком.