пятница, 17 февраля 2023 г.

[prog.c++] Вроде бы придумался хороший вопрос про C++ для собеседований

Вчера довелось заглянуть в cppreference в описание стандартного класса std::reference_wrapper. И взгляд зацепился за показанную там возможную реализацию этого класса:

namespace detail {
template <class T> constexpr T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}
 
template <class T>
class reference_wrapper {
public:
  // types
  using type = T;
 
  // construct/copy/destroy
  template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>()
  )>
  constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
  reference_wrapper(const reference_wrapper&) noexcept = default;
 
  // assignment
  reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
 
  // access
  constexpr operator T& () const noexcept { return *_ptr; }
  constexpr T& get() const noexcept { return *_ptr; }
 
  templateclass... ArgTypes >
  constexpr std::invoke_result_t<T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const 
    noexcept(std::is_nothrow_invocable_v<T&, ArgTypes...>) 
  {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }
 
private:
  T* _ptr;
};

Внимание мое привлекли реализации конструкторов для reference_wrapper. Как-то сурово там все, сходу и не поймешь. Т.е. приблизительно понятно почему так сложно, но вот как именно это должно работать... За пару-тройку минут и не въедешь.

Чуть позже, уже по дороге домой, посредством неспешных размышлений картинка выстроилась, вроде бы все встало на свои места. И как бы и не сложно там все. Хотя и страшно это конечно же выглядит.

Но потом подумалось вот что: а ведь если на собеседовании по C++ у соискателя попросить объяснить почему у предложенной реализации reference_wrapper такие конструкции и как именно это работает, то получится отличный вопрос на знание C++.

Действительно, тут тебе и выбор между шаблоном и не шаблоном, и SFINAE, и std::declval, и оператор запятая, и тип выражения, в котором оператор запятая используется, и про noexcept, и еще раз про noexcept, но уже другой, и std::addressof вместо &, и еще всякое разное по мелочи.

Как по мне, так если человек слабо знает C++, то он толком ничего объяснить не сможет.

Если знания C++ средненькие, как у меня, например, то потихоньку выплывет. Заодно интервьюер сможет послушать как соискатель размышляет в процессе. Это же, типа, очень важно, можно и молодежно, как же без этого ;)

Ну а если перед вами человек, который с C++ на "ты", то для него это вообще проблемы не составит. Может он даже расскажет, как сделать тоже самое более простым и красивым способом (disclaimer: я даже не имею понятия можно ли вообще).

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

Короче говоря, если бы мне на собеседовании такой вопрос задали, то это было бы интереснее, чем запрограммировать в онлайне какую-то около олимпиадную задачку. Не говоря уже про банальности о виртуальном деструкторе :)

четверг, 16 февраля 2023 г.

[prog.wow] Век живи, век учись: с указателем после освобождения ничего нельзя делать

Иногда флеймы на профильных ресурсах оказываются очень полезны. Вот свежий пример с LOR-а: "Навеяно свежей дырой в Xorg".

В двух словах: после освобождения указателя (через free в C или delete в C++) с указателем ничего нельзя делать (кроме как присвоить ему новое валидное значение).

Т.е. не то, что разыменовывать нельзя (это-то как раз понятно), но и вообще ничего нельзя: ни распечатать, ни сравнить с чем-нибудь (пусть даже и с NULL/nullptr), ни преобразовать в uintptr_t. НИЧЕГО. Любые попытки сделать что-то подобное есть UB.

Говорят, что у этого есть даже какое-то логическое обоснование. Но мне осознать сие не получается :)

Вышесказанное означает, что если у вас написано что-то подобное:

void data_cleanup(data * ptr) {
  if(ptr != NULL) {
    ... // Какие-то действия по очистке.
    free(ptr);
  }
  else {
    ... // Какие-то другие действия. Допустим, просто
        // печать в лог о том, что data_cleanup был вызван.
  }

  ... // Еще какие-то действия, которые не зависят от
      // значения ptr.

  // А здесь нужно еще что-то сделать если ptr не был NULL.
  if(ptr != NULL) { // (1)
    ... 
  }
}

то поздравляю, у вас в коде UB. И, судя по тому, как безжалостно компиляторостроители начинают UB эксплуатировать, рано или поздно случится какая-нибудь бяка.

Полагаю, что выйти из ситуации можно вот так:

void data_cleanup(data * ptr) {
  int not_null = ptr != NULL;
  if(not_null) {
    ... // Какие-то действия по очистке.
    free(ptr);
  }
  else {
    ... // Какие-то другие действия. Допустим, просто
        // печать в лог о том, что data_cleanup был вызван.
  }

  ... // Еще какие-то действия, которые не зависят от
      // значения ptr.

  // А здесь нужно еще что-то сделать если ptr не был NULL.
  if(not_null) {
    ... 
  }
}

Но это же нужно быть очень внимательным, чтобы не наступать на подобные грабли.

К счастью, в C++ при использовании умных указателей вроде shared_ptr и unique_ptr все не так страшно. Но вот если нужно записать что-то на чистой ламповой Сишечке или на старых плюсах, в которых умных указателей нет... То грустно.


Данное обсуждение заставило вспомнить еще про одну засаду с голыми указателями. Еще, если кто-то не знал или забыл, в Си и C++ нельзя просто так сравнивать на больше/меньше указатели одного типа. Грубо говоря, если у нас есть указатели a и b одного типа, то выражение (a<b) будет определено, только если a и b указывают на элементы одного массива (либо на элемент за последним элементом этого массива).

Правда, в C++, насколько я понимаю, можно воспользоваться std::less или std::greater из стандартной библиотеки. Поскольку для подобных компараторов определены специализации для указателей. Например, по поводу std::less на cppreference сказано буквально следующее: "A specialization of std::less for any pointer type yields the implementation-defined strict total order, even if the built-in < operator does not."

Вот ведь, а я всю жизнь указатели одного типа сравнивал между собой и даже не задумывался. Подобное сравнение есть, например, в SObjectizer-е. Нужно будет в следующем релизе поправить.

понедельник, 13 февраля 2023 г.

[prog.c++.flame] Наглядный пример того, за что я не люблю C++

Хорошая статья на английском, "Behind the magic of magic_enum", которая объясняет принципы работы библиотеки magic_enum (у которой, мать-мать-мать, 3.4K звезд на GitHub-е).

Статья-то хорошая, но она как раз иллюстрирует то, за что я не люблю C++ (и как язык, и как экосистему, и как коммьюнити). А именно:

Вместо того, чтобы добавить нужную фичу непосредственно в язык люди пытаются эмулировать ее кривыми и убогими велосипедами из говна и палок.

И, что самое страшное, это и вызывает восхищение, и находит применение.

Нечто подобное уже было в прошлом, когда в Boost включили Boost.Lambda, эмулировавшую лямбда-функции для C++98. Убогая, страшная как черт, требовавшая нехилых познаний в C++ для того, чтобы разобраться в деталях ее реализации, тормозившая компиляцию и провоцировавшая internal compiler errors... Но использовавшаяся!

И вот этого я не понимал ни тогда, ни сейчас.

Ну нет в языке фичи X. Смиритесь и обходитесь без нее. Да, придется больше написать ручками. Но зато это будет работать вне зависимости от компилятора и понятно будет даже начинающему C++нику (или деду вроде меня, который уже мало чего помнит).

Но нет, давайте налепим известную субстанцию на палочку, обзовем это конфеткой и будем жрать причмокивая.