Pull to refresh
88
5.2
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

Send message

очевидно, над ней ещё надо работать, потому что из неё совершенно неочевидно, что она хочет, чтобы вторым параметром в node_t был А.

В C++17, насколько я знаю, нет нормальных возможностей сформировать в compile-time динамическую строку для static-assert-а.

Кроме того, тут имеет смысл предъявить претензии к качеству диагностики VC++, поскольку GCC более вменяем:

Compiling ./_habr_q001.cpp ...
In file included from ./_habr_q001.cpp:1:
./so_5_extra/msg_hierarchy/pub.hpp: In instantiation of 'so_5::extra::msg_hierarchy::node_t<Derived, Base>::node_t(Derived&) [with Derived = D; Base = B]':
./_habr_q001.cpp:22:42:   required from here
./so_5_extra/msg_hierarchy/pub.hpp:375:54: error: static assertion failed
  375 |                                                 std::is_base_of_v<Base, Derived> );
      |                                                 ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
./so_5_extra/msg_hierarchy/pub.hpp:375:54: note: 'std::is_base_of_v<B, D>' evaluates to false

Я правильно понимаю, что без дополнительных классов вы не можете пройти по иерархии наследования и подписаться одновременно на typeid(Base) и typeid(Derived), чтобы получать сообщения от обеих типов.

Да, в C++ пока нет стандартной рефлексии, поэтому имея на руках тип Derived нет возможности узнать кто у него базовый класс.

А с вашим node_t можете?

Да.

А интересно, что будет, если иерархия в node_t будет отличаться от реальной. Например, вы укажете, что B наследует от C, когда на самом деле он наследует от A.

Если вы про ситуацию типа вот такой:

namespace hierarchy_ns = so_5::extra::msg_hierarchy;

struct B : public hierarchy_ns::root_t<B>
	{
		B() = default;
	};

struct A : public hierarchy_ns::root_t<A>
	{
		A() = default;
	};

struct D
	: public A
	, public hierarchy_ns::node_t< D, B >
	{
		D()
			: hierarchy_ns::node_t< D, B >( *this )
			{}
	};

то будет ошибка компиляции:

Compiling ./_habr_q001.cpp ...
_habr_q001.cpp
.\so_5_extra/msg_hierarchy/pub.hpp(374): error C2607: static assertion failed
.\so_5_extra/msg_hierarchy/pub.hpp(374): note: the template instantiation context (the oldest one first) is
./_habr_q001.cpp(19): note: see reference to class template instantiation 'so_5::extra::msg_hierarchy::node_t<D,B>' being compiled
.\so_5_extra/msg_hierarchy/pub.hpp(371): note: while compiling class template member function 'so_5::extra::msg_hierarchy::node_t<D,B>::node_t(Derived &)'
        with
        [
            Derived=D
        ]
./_habr_q001.cpp(22): note: see the first reference to 'so_5::extra::msg_hierarchy::node_t<D,B>::node_t' in 'D::D'

но, наверное, можно придумать что-то поизощреннее, чтобы обойти простые проверки.

Мешает идентификация сообщений. Допустим, у нас есть Base и Derived, где Derived унаследован от Base.
Тогда typeid(Base) будет отличаться от typeid(Derived).
Когда в SObjectizer отсылается сообщение, то оно сопровождается идентификатором типа отосланного сообщения. Т.е. когда в коде написано send<Base>(...), то экземпляр сообщения будет сопровождать typeid(Base). И подписка будет искаться именно по typeid(Base). Поэтому если кто-то отошлет send<Derived>(...) то подписка на Base не сработает, т.к. typeid(Base) != typeid(Derived).

Можно отослать Derived как Base. Что-то типа:

auto d = std::make_unique<Derived>(...);
so_5::message_holder_t<Base> msg{std::move(d)};
so_5::send(..., std::move(msg));

Но тогда SObjectizer будет видеть это сообщение как Base. Поэтому если кто-то сделал подписку на Derived, то такое сообщение он не получит.

А зачем потребовалась рукопашная работа с ID для типов при наличии std::type_index?

Показанный в статье вариант со SCOPE_EXIT -- это творение какого-то сумрачного разума, в котором invoke вынуждены вызывать дважды, причем в первом случае все равно зачем-то делают сохранения результата вызова в auto result.

Так что нет, не вижу эквивалентности.

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

Почему нельзя было сделать так:

class test_duration_meter {
  const pcstr m_test_name;
  const time_point m_started_at;
public:
  explicit test_duration_meter(pcstr test_name)
    : m_test_name{test_name}
    , m_started_at{gtl::high_resolution_clock::now()}
  {}
  ~test_duration_meter() {
    const auto finished_at = gtl::high_resolution_clock::now();
    gtl::duration<double> diff = finished_at - m_started_at;
    test_printf("Test %s spent time %ds \n", m_test_name, diff.count());
  }
};

template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args)
{
    test_duration_meter test_duration{test_name};
    return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
}

Она опубликована в трех хабах, один из них - это C++.

Ну и в С++ она выглядит чужеродно.

Java по этому критерию отпадает сразу - в этом аспекте она не может тягаться с C++

Тут нужно смотреть на конкретные сценарии. Это в 1990-е Java однозначно сливала C++у. Уже во второй половине нулевых для server-side задач все было уже не так однозначно.

Go: по своей парадигме это больше процедурный язык, чем объектно-ориентированный, на нем проблематично реализовывать сложные модели.

Из того, что вы описали в статье и из приведенных примеров кода вообще не видно, что где-то были бы нужны сложные модели.

Например, вы знаете на расте аналог libsodium ?

libsodium, насколько я знаю, это чистый Си. И биндинги для Rust-а к libsodium перечислены прямо на сайте libsodium.

Даже не знаю, что сказать, чтобы не обидеть автора. Ведь работа им была проделана титаническая, это, пожалуй, самая объемная статья на Хабре из мной прочитанных. Но...

...но есть ощущение, что она была помещена не в тот раздел, т.к. конкретно о C++ или о применении C++ для такого рода задач из статьи ничего нового или хотя бы сколько-нибудь интересного узнать не удалось. Даже не было сказано почему именно C++ (а не Rust, Go, Java или какой-нибудь другой ЯП) был выбран для разработки, какие преимущества дал, какие проблемы привнес и т.д., и т.п.

В общем, колоссальная работа была проделана и автору респект, но ее следовало бы в другой хаб публиковать.

Мне кажется, или в этом фрагменте кода:

//command contains preliminary parsed json; it is a map of <json-key> <json-value>
void cmd_send (const command& cmd) 
{ 
    auto account = cmd.args().find("account"); 

    if (account == cmd.args().end()) //account is mandatory
        return cmd.make_reply(error_t::param_count); 

    tx_send tx;
    // ref is a parameter parser. We assign which type is allowed
    param acc = param::parse(account, pt_id | pt_pk | pt_name); 

    tx.acc = get_account(param); //depending on type we search appropriate id

внезапно появилась новая переменная param, которой ранее не было, но не использовалась переменная acc?

А C++ уже потом чисто "бонусом" идёт.

Как правило, у тех, у кого он идет "чисто бонусом", этот самый C++ получается таким, что лучше уж бонусом шло бы что-то другое. Исключения, конечно же, случаются. Бывают что и математики, и физики пишут отличный код. Но это именно что исключения.

Простите, не удержал свою боль внутри :(

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

Но это не повод не показать, что некоторые "выводы", скажем так, совсем не интуитивны и больше походят на заблуждения.

Чистый код, и соответствие принципу KISS (по моему, это одно и тоже)

Да?

Допустим, это KISS, ибо проще некуда:

class some_thread_safe_type {
  std::mutex m_lock;
  some_data m_data;
...
public:
  void modify() {
    m_lock.lock();
    m_data.modify();
    m_lock.unlock();
  }
 ...
};

А вот это чистый код:

class some_thread_safe_type {
...
public:
  void modify() {
    std::lock_guard lock{m_lock};
    m_data.modify();
  }
...
};

Одно и тоже же ж.

ЗЫ. Не могу удержаться:

	class IPasswordHash
	{
	public:
		virtual std::string get(std::string pass) = 0;
	};

Чем обосновывается передача pass в get по значению? Или это тоже магическая часть "чистого" кода?

Например, стратегией может быть наличие affinity треда пушащего работу с каким-либо тредом внутри тредпула

Может проще было эту мысль выразить непосредственно на английском языке? Зачем этот жалкий транслит русскими буквами?

В нашем примере, если автор сторонней библиотеки предоставляет интерфейс, совместимый с std::execution

Вообще-то здесь сразу два "если":

  • если автор библиотеки выставляет наружу интерфейс для интеграции с использованным внутри библиотеки тред-пулом;

  • если этот самый интерфейс совместим с std::execution.

Можно предположить, что в большом количестве случаев все закончится еще на первом "если".

Я понимаю. Но моя точка зрения, что пользователей языка на пару порядков больше, чем компиляторописателей, а их средний уровень гораздо ниже. Если сделать лучше нескольким миллионам пользователей языка ценой ущерба для нескольких тысяч разработчиков компиляторов, то это было бы разумно. ИМХО.

их аргументы сходу отметать, видимо, не получается.

Скорее всего так и есть.

UB - это такая штука, да. Сегодня работает, а завтра нет.

Так ведь есть и другой способ: перестать считать какое-то поведение UB и легализовать его.

ибо любой UB может работать довольно долго, пока не перестанет

Боюсь, здесь нужно начинать разбираться в сортах говна.
Есть очевидные UB: чтение из неинициализированных переменных, обращение по невалидным указателям, type-puning через union и пр. Что характерно, объяснить почему так делать -- это фу-фу-фу -- не так уж и сложно.

Есть менее очевидные, но все же вполне объясняемые UB. Вроде переполнения знаковых целых. Что характерно, объяснить почему это таки UB, и почему нельзя поступить как с беззнаковыми уже тяжелее.

И есть совсем неочевидные UB. Ну вот типа этих самых lifetime. Тут сразу две проблемы:

a) про них вообще мало кто знает;
b) даже тем, кто про такие UB знает, непросто объяснить другим почему же это UB.

Типа такого:

alignas(Demo) char raw_bytes[sizeof(Demo)];
read_from_some_source(raw_bytes, sizeof(Demo));
Demo * d = reinterpret_cast<Demo *>(raw_bytes);

Это же, в принципе, прямой донельзя код. Причем, судя по декларациям назначения C++, это как раз такой код, который должен писаться на C++ легко и непринужденно.

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

И это при том, что без малого 40 лет до появления C++23 все это работало, а тут вдруг все, бабушка приехала (c)

Как я и говорю - недовольные всегда найдутся.

ИМХО, некоторое недовольство, высказанное вслух, может быть конструктивным, т.к. может указывать, что направление движения выбрали не туда.

Сделали оптимизации - нашлись другие недовольные и т.д.

Как раз такое недовольство понятно: взяли и поломали то, что работало до того как. То же, что дали, не покрывает все проблемы. Т.е. взялись делать "хорошо" и недодумали. О чем и речь.

Нет, не безопасно.

Небезопасно с точки зрения буквы стандарта. Но если компилятор в рамках C++17 начнет ломать код, который требовал бы start_lifetime_as из C++23, то это уже вредительство, т.к. возможностей C++17 для покрытия этих ситуаций не хватает. Ну так и нечего ломать то, что работало до.

Т.е. должно работать негласное правило -- да, есть такой UB, но компилятор в рамках стандарта X не должен его эксплуатировать, т.к. устранить этот UB у программиста возможности нет.

Если такого правила нет, значит и комитет, и писатели компиляторов загоняют еще один гвоздь в крышку гроба C++.

Ну сейчас правила еще более просты: если уже тем или иным образом время жизни объекта внутри некоего массива байт уже началось - std::launder. Если еще не началось - std::start_lifetime_as.

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

Чисто ради этого старого кода.

Да я как бы в курсе. Только вот что делать, если в проекте у меня вместо malloc-а своя функция для аллокации, а вместо read или memcpy -- своя функция побайтового копирования из COM-порта. Получается, что все звери равны, но некоторые равнее.

Как ни сделай, все равно кто-то недовольный да найдется

Так ведь это же нововведения в C++17 и C++20 довели до текущего состояния. Сперва в язык добавили std::launder и сделали вне закона практику, которой был уже не один десяток лет. Но выяснилось, что std::launder не решает всех проблем, т.е. есть еще и создание типов через malloc (унаследованная из Си практика), и десериализация POD-типов в байтовый буфер.

Поэтому в C++20 попробовали это исправить. Ввели неявное начало лайфтайма.
Но выяснилось, что это так же не все покрывает.

Поэтому в C++23 ввели еще и std::start_lifetime_as.

А в таких условиях писать код "уже сейчас" становится вообще еще тем квестом. Скажем, если я живу в рамках 17-го стандарта и у меня есть только std::launder, то мне безопасно вставлять std::launder везде. Если же мой код затем перекомпилируют в рамках C++23, то вдруг окажется, что где-то нужен не std::launder, а std::start_lifetime_as.

При этом если я захочу разобраться а как же должно быть правильно в рамках C++23, то остаются неясными предпосылки для std::launder и std::start_lifetime_as. Советы менять архитектуру идут по известному адресу, т.к. все, блин умные, пока реальный код писать или приводить в рабочее состояние не нужно.

ЗЫ. Собственно, чего хотелось бы иметь:

  • чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A был создан новый объект A (или какой-то отнаследованный от него B), старые указатели "протухли", нужно их отмыть через std::launder. Все. Больше ни для чего std::launder не нужен;

  • чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.

Все. Без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.

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

1
23 ...

Information

Rating
936-th
Location
Гомель, Гомельская обл., Беларусь
Registered
Activity