Проблеми TypeScript у світі React-додатків вiд Iллi Климова на React fwdays | 27 березня
×Закрыть

Чепурні мультиметоди для сучасного С++

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Мультиметоди, або ж множинна диспетчеризація, це механізм вибору однієї з декількох функцій в залежності від динамічних типів або значень аргументів[1]. Потреба в такому механізмі виникає, наприклад, в архітектурних рішеннях, де численні класи взаємодіють між собою у специфічний для кожної пари спосіб. C++ на рівні мови не підтримує такий механізм а пропозиції щодо розширення C++ такими інструментами[2] (поки що?) не включені до попереднього плану C++23 [11].

Оскільки потреба в множинній диспетчеризації стара як світ програмування, створено чимало рішень з використанням існуючих інструментів C++ (див. [3], [4], [5], [6], [7], [8], [9], [10]). Проте жодне з них не має чепурного вигляду. Безперечно, чепурність — категорія суб’єктивна і, на думку автора, чепурне рішення це щось просте, сучасне, необтяжливе, неінтрузивне та структуроване. Тобто складність такого рішення відповідає складності задачі, його використання не привносить додаткових залежностей та не ускладнює підтримку коду, не вимагає істотних змін у дизайні проєкту, і кожен елемент рішення вирішує одну потребу[12] чи відповідає за один обов’язок[13].

Метою цієї статті не є просування готового рішення, що підійшло б усім і на усі випадки. Натомість у цій статті ми з вами розглянемо, які підходи можна використати для різних сценаріїв використання.

Для динамічної, часу виконання, диспетчеризації потрібно знати тип об’єкту. C++ має механізми RTTI які можна використати для тих проєктів, що вже використовують RTTI чи можуть собі дозволити таку залежність. Проте рішення не буде чепурним, якщо не надасть альтернативу для проектів, де використання RTTI було б занадто обтяжливим.

Традиційно, подвійна диспетчеризація виконується через віртуальний метод, у якому реалізована ручна диспетчеризація наступного рівня через оператори if чи switch. Хоча, з точки зору моделювання даних, ці методи переважно не є природною частиною класів, де вони реалізовані, а радше визначають зовнішні операції над класами об’єктів. То ж вони були б більш органічно представлені як функції[2]. Проте ми не будемо відкидати можливість використання методів.

Моделі даних. У цій статті ми будемо вправлятись на ієрархії фігур та ієрархії виразів калькулятора:

namespace shapes {
struct Shape {
	virtual ~Shape() {}
};
struct Rect : Shape { };
struct Circle : Shape {};
struct Square : Rect { };
}

namespace calculus {
struct Expression {
	virtual ~Expression() {}
};

struct Constant : Expression {};
struct Integer : Constant {};
struct Float : Constant {};
}

Почнемо із моделі calculus. Припустимо, що визначені операції add, sub, і ці операції реалізовані як шаблонні функції:

шаблонні функції:
template<class A, class B>
Expression* add(const A&, const B&);
template<class A, class B>
Expression* sub(const A&, const B&);

Це дещо спростить перший крок, а ускладнювати будемо поступово.

Отже, наш мультиметод має з’ясувати фактичні типи аргументів, базуючись на цих типах вибрати потрібну функцію із наявних, та викликати обрану функцію. Тут ми окреслили три потреби. Розглянемо їх по черзі.

Вибір функції із наявних. Для створення списку цих функцій використаємо варіадичний шаблон із auto параметрами, щоб використати можна було таким чином:

multidispatcher<
  add<Integer, Integer>,
  add<Float, Integer>,
  add<Integer, Float>,
  add<Float, Float>>::dispatch(a,b);

Для такого використання наш шаблон має виглядати десь так:

template<auto Entry, auto ... Entries>
struct multidispatcher {
  template<class ... Arguments>
  static auto dispatch(Arguments& ... arguments) {
    if (details::entry<Entry>::matches(arguments...)) {
      return details::entry<Entry>::call(arguments...);
    }
    if constexpr (sizeof...(Entries)>0) {
      return multidispatcher<Entries...>::dispatch(arguments...);
    }
  }
};

У методі dispatch ми делегували визначення фактичного типу та виклик функції іншому шаблону entry, давайте його напишемо:

namespace details {
template<auto Method>
struct entry;

template<class Return, class ... Parameter, Return (*Function)(Parameter...)>
struct entry<Function> {
  template<class ... Argument>
  static constexpr bool matches(const Argument& ... argument) noexcept {
    return (expected<Parameter>::matches(argument) and ...);
  }
  template<class ... Argument>
  static Return call(Argument& ... argument) noexcept(noexcept(Function)) {
    return (*Function)((Parameter)(argument)...);
  }
};
}

Шаблон expected, який ми тут використали, буде визначати тип фактичного аргументу та чи відповідає цей тип очікуваному. Таким чином повна реалізація набуде такого вигляду

namespace details {

template<class Parameter>
struct expected {
  template<class Argument>
  static constexpr bool matches(const Argument& argument) noexcept {
    return typeid(Parameter) == typeid(argument);
  }
};
template<auto Method>
struct entry;

template<class Return, class ... Parameter, Return (*Function)(Parameter...)>
struct entry<Function> {
  template<class ... Argument>
  static constexpr bool matches(const Argument& ... argument) noexcept {
    return (expected<Parameter>::matches(argument) and ...);
  }
  template<class ... Argument>
  static Return call(Argument& ... argument) noexcept(noexcept(Function)) {
    return (*Function)((Parameter)(argument)...);
  }
};
} //namespace details

template<auto Entry, auto ... Entries>
struct multidispatcher {
  template<class ... Arguments>
  static auto dispatch(Arguments& ... arguments) {
    if (details::entry<Entry>::matches(arguments...)) {
      return details::entry<Entry>::call(arguments...);
    }
    if constexpr (sizeof...(Entries)>0) {
      return multidispatcher<Entries...>::dispatch(arguments...);
    }
  }
};

Поекспериментувати із цією реалізацією можна за посиланням.

Реалізація вийшла доволі проста, проте, порівнюючи із open methods, вона має певні обмеження:

  1. довжина списку функцій обмежена максимальною глибиною рекурсії компілятора
  2. не підтримується коваріантність результату
  3. не підтримується коваріантність параметрів
  4. вибирається не найкраща як в open method, а перша-ліпша функція
  5. немає поділу на virtual та звичайні параметри — усі очікуються virtual

Через друге обмеження ми не можемо використати функцію що повертає коваріантний результат, наприклад таку: Float* sub(const Float&, const Float&). Третє, четверте та п’яте обмеження дещо звужують сферу сценаріїв використання. У одному із наступних розділів ми розглянемо як їх обійти. Поки що повернемось до більш критичних обмежень 1 та 2.

Щоб підтримати коваріантність результату нам необхідно задати найбільш загальний тип результату із усіх використаних у функціях. Створимо новий шаблон із додатковим параметром — типом результату:

template<typename ReturnType, auto ... Entries>
struct multimethod;

Раз у нас вже є тип результату, ми можемо використати його також щоб замінити рекурсію на згортку параметрів шаблону:

template<typename ReturnType, auto ... Entries>
struct multimethod {
  template<class ... Arguments>
  static auto dispatch(Arguments& ... arguments) {
    ReturnType value;
    if (((details::entry<Entries>::matches(arguments...)
      and ((value = details::entry<Entries>::call(arguments...)),true)) or ...))
      return value;
    else
      throw std::logic_error("Missing dispatch entry");
  }
};

Лінк на модифікований приклад.

Зазначимо, що такий підхід потребує підтримки move семантики для типу результату. Для нашого прикладу це не суттєво, проте для інших сценаріїв використання слід враховувати цю особливість.

Допитливий читач можливо звернув увагу що в останньому прикладі не використовується перевантаження функції sub:

Float* subf(const Float&, const Float&);
Integer* subi(const Integer&, const Integer&);

Це через особливості C++ для перевантажених функцій. Щоб отримати адресу такої функції потрібно задати її тип: (Float* (*)(const Float&, const Float&))&sub. Цей незручний синтаксис можна спробувати трохи підсолодити шаблоном resolve: resolve<Float*, const Float&, const Float&>{}(&sub).

Розглянемо тепер як можна обійти залежність від RTTI, що виникла через наше використання typeid. Для того щоб підтримати сценарії використання без RTTI, прикладна модель має реалізовувати якийсь механізм користувацької ідентифікації та інтроспекції типів (КІІТ). Наприклад, це може бути призначений вручну чи автогенерований числовий ID для кожного класу. Для автогенерації можна використати геш від __PRETTY_FUNCTION__:

template<class Class>
constexpr auto class_hash() noexcept {
  return hash<>(std::string_view(__PRETTY_FUNCTION__));
}

Нажаль, std::hash ще не є constexpr, тож доведеться написати і свою геш функцію (наприклад, таку як в [14]). Тепер ми можемо призначати ID нашим класам:

  static constexpr auto classid = class_hash<Rect>();

Для доступу до classid напишемо шаблонну функцію classinfo:

using class_info = size_t;
template<class Class>
class_info classinfo() noexcept {
  return Class::classid;
}

А для динамічної інтроспекції типів створимо віртуальний метод :

 //Shape
  virtual bool instance_of(const class_info& expected) const noexcept {
    return classinfo<decltype(*this)>() == expected;
  }

  //Rect, Circle
  bool instance_of(const class_info& expected) const noexcept override {
    return classinfo<decltype(*this)>() == expected or Shape::instance_of(expected);
  }

А щоб його використання не створювало залежності від користувацького типу class_info, заховаємо його за таким фасадом:

  //Shape
  template<class Expected>
  bool instanceof() const noexcept {
    return instance_of(classinfo<Expected>());
  }

Зверніть увагу, що ці методи не перевантажені. Це спростить допоміжний шаблон has_instanceof.

Поки ми використовували RTTI, можна було ігнорувати відмінності між віртуальними і звичайними параметрами — typeid працює із усіма типами, а додаткова перевірка на співпадіння статичних типів прибирається із вихідного коду оптимізатором. Проте із КІІТ нам слід вирішити як розпізнати невіртуальні параметри та як порівнювати типи для них. Для розпізнавання можна встановити таке правило: параметри поліморфічних типів, що передаються як lvalue reference є віртуальними, усі інші — не є такими:

template<class Class>
struct is_virtual_parameter {
  static constexpr bool value = 
       std::is_polymorphic_v<std::remove_reference_t<Class>> and
       std::is_reference_v<Class>;
};

А для перевірки невіртуальних параметрів це може бути, наприклад, is_same, чи is_assignable. Виходячи з такого дизайну КІІТ, модифікуємо наш шаблон expected:

template<class Parameter>
struct expected {
  using parameter_type = std::remove_reference_t<std::remove_cv_t<Parameter>>;
  template<class Argument>
  static constexpr bool matches(Argument&& argument) noexcept {
    if constexpr(has_instanceof<Argument>(0)) {
      return argument.template instanceof<parameter_type>();
    } else {
#if __cpp_rtti >= 199711
      return typeid(Parameter) == typeid(argument);
#else
      static_assert(not is_virtual_parameter_v<Parameter>, "No class info available");
      return is_assignable_parameter_v<Parameter, Argument>;
#endif
    }
  }
};

Поки що наш multimethod шаблон підтримує лише функції. Напишемо підтримку для методів. Перше що необхідно — це відділити об’єкт від решти аргументів, наприклад створивши новий метод що б приймав об’єкт першим аргументом:

// multimethod
template<class Target, class ... Arguments>
static auto call(Target& target,Arguments& ... arguments) {
  ReturnType value;
  if (((details::entry<Entries>::matches(target, arguments...)
    and ((value = details::entry<Entries>::call(target, arguments...)),true))
    or ...))
    return value;
  else
    throw std::logic_error("Dispatcher failure");
}

Та додати спеціалізацію entry для методів:

template<class Target, class Return, class ... Parameter, Return (Target::*Method)(Parameter...)>
struct entry<Method> {
  template<class Object, class ... Argument>
  static constexpr bool matches(Object& obj, Argument& ... argument) noexcept {
    return expected<Target>::matches(obj)
       and (expected<Parameter>::matches(argument) and ...);
  }
  template<class Object, class ... Argument>
  static Return call(Object& target, Argument& ... argument) {
    return ((Target&)(target).*Method)((Parameter&)(argument)...);
  }
};

З методами трохи складніше ніж із функціями, бо їх сигнатура може включати також специфікатор const. Тож знадобиться дві спеціалізації нашого entry, та два методи call. Код для цього прикладу доступний за посиланням.

Завдяки нашій реалізації КІІТ з викликом instance_of базового класу ми також вирішили проблему коваріантності параметрів. Проте щоб вона працювала як слід, першими у списку мають іти функції/методи із більш специфічними параметрами. Впорядкувати список під час компіляції не видається можливим. Натомість, можна додати перевірку часу компіляції — перевіряти чи далі по списку функцій відсутні такі що приймають породжений від поточного клас. Складність такої перевірки можна оцінити як O(kn²) де k кількість параметрів а n — кількість функцій. Слід зауважити, що на великих списках така перевірка може уповільнити компіляцію.

Крім того підтримка довгих списків функцій може стати занадто обтяжливою. Щоб спростити роботу із великою кількістю функцій можна розбити список на групи по однотипному першому параметру, наприклад так:

groupdispatcher<
  group<Expression*,
    add<Integer, Float>,
    add<Integer, Integer>>,
  group<Expression*,
    add<Float, Integer>,
    add<Float, Float>>>::dispatch(a,b);

Можна також повернутись до типового рішення подвійної диспетчеризації через віртуальні методи у якому використати наші чепурні шаблони для наступного рівня диспетчеризації.

Прикінцеві нотатки

  1. У прикладах, викладених на godbolt.org компілятор gcc генерує оптимізований код із статичною диспетчеризацією. Щоб була задіяна, динамічна диспетчеризація слід рознести test функції прикладів по різних одиницях компіляції.
  2. Приклади із цієї статті також доступні на gist.github.com:
    calculus multidispatcher
    calculus multifunction
    shapes multimethod
  3. Розглянуте рішення також доступне у вигляді include-only бібліотеки:
    github.com/hutorny/multimethods

Перелік джерел

  1. Мультиметод. Вікіпедія.
    https://uk.wikipedia.org/wiki/Мультиметод
  2. P. Pirkelbauer, Y. Solodkyy, B. Stroustrup,
    Open Multi-Methods for C++,
    www.stroustrup.com/multimethods.pdf
  3. Elsevier B.V.,
    EVL: A framework for multi-methods in C++,
    www.sciencedirect.com/...​cle/pii/S0167642314003360
  4. D. Shopyrin,
    MultiMethods in C++: Finding a complete solution,
    www.codeproject.com/...​nding-a-complete-solution
  5. Loki Library, Multiple dispatcher,
    sourceforge.net/projects/loki-lib Reference/MutiMethod.h
  6. L. Bettini,
    Doublecpp — double dispatch in C++,
    doublecpp.sourceforge.net
  7. J. Smith,
    Draft proposal for adding Multimethods to C++,
    www.open-std.org/...​cs/papers/2003/n1529.html
  8. stfairy,
    Multiple Dispatch and Double Dispatch,
    www.codeproject.com/...​patch-and-Double-Dispatch
  9. J. Smith,
    Multimethods,
    www.op59.net/...​cu-2003-multimethods.html
  10. D. Shopyrin,
    Multimethods in C++ Using Recursive Deferred Dispatching
    www.computer.org/...​2006/03/s3062/13rRUxBa5ve
  11. V. Voutilainen
    To boldly suggest an overall plan for C++23
    www.open-std.org/...​/papers/2019/p0592r4.html
  12. Wikipedia,
    Separation of Concerns
    en.wikipedia.org/...​ki/Separation_of_concerns
  13. Вікіпедія,
    Принцип_єдиного_обов’язку,
    https://uk.wikipedia.org/wiki/Принцип_єдиного_обов’язку
  14. E. Hutorny,
    FNV-1b hash function with extra rotation
    gist.github.com/...​7255f842fae08e542f00131b5
👍НравитсяПонравилось9
В избранноеВ избранном1
Подписаться на тему «C++»
LinkedIn

Похожие статьи

Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

а в предложенной «решаемой задачи» к этому решению

Є 20 конкрентих класів зрупованих по 4-х суперкласах породжених від одного абстракного — усього 25 класів.
На них визначено 5 операцій: add(X,Y), sub(X,Y), mul(X,Y), div(X,Y), polynomial(X,Y,N)
Для кожної із цих операцій визначено по 40 конретних реалізацій — усього 200 функцій.

описание «мультиметода» будет выглядеть примерно так

	inline Shape* Shape::join(const Shape& shape) const {
		return multimethod < Shape*,
			resolve<Shape*, const Henagon&>{}(&Henagon::join),
			resolve<Shape*, const Digon&>{}(&Henagon::join),
			resolve<Shape*, const Triangle&>{}(&Henagon::join),
			resolve<Shape*, const Parallelogram&>{}(&Henagon::join),
			resolve<Shape*, const Rhombus&>{}(&Henagon::join),
			resolve<Shape*, const Rectangle&>{}(&Henagon::join),
			resolve<Shape*, const Square&>{}(&Henagon::join),
			resolve<Shape*, const Trapezoid&>{}(&Henagon::join),
			resolve<Shape*, const Pentagon&>{}(&Henagon::join),
			resolve<Shape*, const Hexagon&>{}(&Henagon::join),
			resolve<Shape*, const Hexapod&>{}(&Henagon::join),
			resolve<Shape*, const Heptagon&>{}(&Henagon::join),
			resolve<Shape*, const Octagon&>{}(&Henagon::join),
			resolve<Shape*, const Nonagon&>{}(&Henagon::join),
			resolve<Shape*, const Decagon&>{}(&Henagon::join),
			resolve<Shape*, const Hendecagon&>{}(&Henagon::join),
			resolve<Shape*, const Dodecagon&>{}(&Henagon::join),
			resolve<Shape*, const Tridecagon&>{}(&Henagon::join),
			resolve<Shape*, const Tetradecagon&>{}(&Henagon::join),
			resolve<Shape*, const Circle&>{}(&Henagon::join),
			resolve<Shape*, const Henagon&>{}(&Digon::join),
			resolve<Shape*, const Digon&>{}(&Digon::join),
			resolve<Shape*, const Triangle&>{}(&Digon::join),
			resolve<Shape*, const Parallelogram&>{}(&Digon::join),
			resolve<Shape*, const Rhombus&>{}(&Digon::join),
			resolve<Shape*, const Rectangle&>{}(&Digon::join),
			resolve<Shape*, const Square&>{}(&Digon::join),
			resolve<Shape*, const Trapezoid&>{}(&Digon::join),
			resolve<Shape*, const Pentagon&>{}(&Digon::join),
			resolve<Shape*, const Hexagon&>{}(&Digon::join),
			resolve<Shape*, const Hexapod&>{}(&Digon::join),
			resolve<Shape*, const Heptagon&>{}(&Digon::join),
			resolve<Shape*, const Octagon&>{}(&Digon::join),
			resolve<Shape*, const Nonagon&>{}(&Digon::join),
			resolve<Shape*, const Decagon&>{}(&Digon::join),
			resolve<Shape*, const Hendecagon&>{}(&Digon::join),
			resolve<Shape*, const Dodecagon&>{}(&Digon::join),
			resolve<Shape*, const Tridecagon&>{}(&Digon::join),
			resolve<Shape*, const Tetradecagon&>{}(&Digon::join),
			resolve<Shape*, const Circle&>{}(&Digon::join),
			resolve<Shape*, const Henagon&>{}(&Triangle::join),
			resolve<Shape*, const Digon&>{}(&Triangle::join),
			resolve<Shape*, const Triangle&>{}(&Triangle::join),
			resolve<Shape*, const Parallelogram&>{}(&Triangle::join),
			resolve<Shape*, const Rhombus&>{}(&Triangle::join),
			resolve<Shape*, const Rectangle&>{}(&Triangle::join),
			resolve<Shape*, const Square&>{}(&Triangle::join),
			resolve<Shape*, const Trapezoid&>{}(&Triangle::join),
			resolve<Shape*, const Pentagon&>{}(&Triangle::join),
			resolve<Shape*, const Hexagon&>{}(&Triangle::join),
			resolve<Shape*, const Hexapod&>{}(&Triangle::join),
			resolve<Shape*, const Heptagon&>{}(&Triangle::join),
			resolve<Shape*, const Octagon&>{}(&Triangle::join),
			resolve<Shape*, const Nonagon&>{}(&Triangle::join),
			resolve<Shape*, const Decagon&>{}(&Triangle::join),
			resolve<Shape*, const Hendecagon&>{}(&Triangle::join),
			resolve<Shape*, const Dodecagon&>{}(&Triangle::join),
			resolve<Shape*, const Tridecagon&>{}(&Triangle::join),
			resolve<Shape*, const Tetradecagon&>{}(&Triangle::join),
			resolve<Shape*, const Circle&>{}(&Triangle::join),
			resolve<Shape*, const Henagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Digon&>{}(&Parallelogram::join),
			resolve<Shape*, const Triangle&>{}(&Parallelogram::join),
			resolve<Shape*, const Parallelogram&>{}(&Parallelogram::join),
			resolve<Shape*, const Rhombus&>{}(&Parallelogram::join),
			resolve<Shape*, const Rectangle&>{}(&Parallelogram::join),
			resolve<Shape*, const Square&>{}(&Parallelogram::join),
			resolve<Shape*, const Trapezoid&>{}(&Parallelogram::join),
			resolve<Shape*, const Pentagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Hexagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Hexapod&>{}(&Parallelogram::join),
			resolve<Shape*, const Heptagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Octagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Nonagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Decagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Hendecagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Dodecagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Tridecagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Tetradecagon&>{}(&Parallelogram::join),
			resolve<Shape*, const Circle&>{}(&Parallelogram::join),
			resolve<Shape*, const Henagon&>{}(&Rhombus::join),
			resolve<Shape*, const Digon&>{}(&Rhombus::join),
			resolve<Shape*, const Triangle&>{}(&Rhombus::join),
			resolve<Shape*, const Parallelogram&>{}(&Rhombus::join),
			resolve<Shape*, const Rhombus&>{}(&Rhombus::join),
			resolve<Shape*, const Rectangle&>{}(&Rhombus::join),
			resolve<Shape*, const Square&>{}(&Rhombus::join),
			resolve<Shape*, const Trapezoid&>{}(&Rhombus::join),
			resolve<Shape*, const Pentagon&>{}(&Rhombus::join),
			resolve<Shape*, const Hexagon&>{}(&Rhombus::join),
			resolve<Shape*, const Hexapod&>{}(&Rhombus::join),
			resolve<Shape*, const Heptagon&>{}(&Rhombus::join),
			resolve<Shape*, const Octagon&>{}(&Rhombus::join),
			resolve<Shape*, const Nonagon&>{}(&Rhombus::join),
			resolve<Shape*, const Decagon&>{}(&Rhombus::join),
			resolve<Shape*, const Hendecagon&>{}(&Rhombus::join),
			resolve<Shape*, const Dodecagon&>{}(&Rhombus::join),
			resolve<Shape*, const Tridecagon&>{}(&Rhombus::join),
			resolve<Shape*, const Tetradecagon&>{}(&Rhombus::join),
			resolve<Shape*, const Circle&>{}(&Rhombus::join),
			resolve<Shape*, const Henagon&>{}(&Rectangle::join),
			resolve<Shape*, const Digon&>{}(&Rectangle::join),
			resolve<Shape*, const Triangle&>{}(&Rectangle::join),
			resolve<Shape*, const Parallelogram&>{}(&Rectangle::join),
			resolve<Shape*, const Rhombus&>{}(&Rectangle::join),
			resolve<Shape*, const Rectangle&>{}(&Rectangle::join),
			resolve<Shape*, const Square&>{}(&Rectangle::join),
			resolve<Shape*, const Trapezoid&>{}(&Rectangle::join),
			resolve<Shape*, const Pentagon&>{}(&Rectangle::join),
			resolve<Shape*, const Hexagon&>{}(&Rectangle::join),
			resolve<Shape*, const Hexapod&>{}(&Rectangle::join),
			resolve<Shape*, const Heptagon&>{}(&Rectangle::join),
			resolve<Shape*, const Octagon&>{}(&Rectangle::join),
			resolve<Shape*, const Nonagon&>{}(&Rectangle::join),
			resolve<Shape*, const Decagon&>{}(&Rectangle::join),
			resolve<Shape*, const Hendecagon&>{}(&Rectangle::join),
			resolve<Shape*, const Dodecagon&>{}(&Rectangle::join),
			resolve<Shape*, const Tridecagon&>{}(&Rectangle::join),
			resolve<Shape*, const Tetradecagon&>{}(&Rectangle::join),
			resolve<Shape*, const Circle&>{}(&Rectangle::join),
			resolve<Shape*, const Henagon&>{}(&Square::join),
			resolve<Shape*, const Digon&>{}(&Square::join),
			resolve<Shape*, const Triangle&>{}(&Square::join),
			resolve<Shape*, const Parallelogram&>{}(&Square::join),
			resolve<Shape*, const Rhombus&>{}(&Square::join),
			resolve<Shape*, const Rectangle&>{}(&Square::join),
			resolve<Shape*, const Square&>{}(&Square::join),
			resolve<Shape*, const Trapezoid&>{}(&Square::join),
			resolve<Shape*, const Pentagon&>{}(&Square::join),
			resolve<Shape*, const Hexagon&>{}(&Square::join),
			resolve<Shape*, const Hexapod&>{}(&Square::join),
			resolve<Shape*, const Heptagon&>{}(&Square::join),
			resolve<Shape*, const Octagon&>{}(&Square::join),
			resolve<Shape*, const Nonagon&>{}(&Square::join),
			resolve<Shape*, const Decagon&>{}(&Square::join),
			resolve<Shape*, const Hendecagon&>{}(&Square::join),
			resolve<Shape*, const Dodecagon&>{}(&Square::join),
			resolve<Shape*, const Tridecagon&>{}(&Square::join),
			resolve<Shape*, const Tetradecagon&>{}(&Square::join),
			resolve<Shape*, const Circle&>{}(&Square::join),
			resolve<Shape*, const Henagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Digon&>{}(&Trapezoid::join),
			resolve<Shape*, const Triangle&>{}(&Trapezoid::join),
			resolve<Shape*, const Parallelogram&>{}(&Trapezoid::join),
			resolve<Shape*, const Rhombus&>{}(&Trapezoid::join),
			resolve<Shape*, const Rectangle&>{}(&Trapezoid::join),
			resolve<Shape*, const Square&>{}(&Trapezoid::join),
			resolve<Shape*, const Trapezoid&>{}(&Trapezoid::join),
			resolve<Shape*, const Pentagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Hexagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Hexapod&>{}(&Trapezoid::join),
			resolve<Shape*, const Heptagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Octagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Nonagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Decagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Hendecagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Dodecagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Tridecagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Tetradecagon&>{}(&Trapezoid::join),
			resolve<Shape*, const Circle&>{}(&Trapezoid::join),
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			.
			resolve<Shape*, const Henagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Digon&>{}(&Tridecagon::join),
			resolve<Shape*, const Triangle&>{}(&Tridecagon::join),
			resolve<Shape*, const Parallelogram&>{}(&Tridecagon::join),
			resolve<Shape*, const Rhombus&>{}(&Tridecagon::join),
			resolve<Shape*, const Rectangle&>{}(&Tridecagon::join),
			resolve<Shape*, const Square&>{}(&Tridecagon::join),
			resolve<Shape*, const Trapezoid&>{}(&Tridecagon::join),
			resolve<Shape*, const Pentagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Hexagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Hexapod&>{}(&Tridecagon::join),
			resolve<Shape*, const Heptagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Octagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Nonagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Decagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Hendecagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Dodecagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Tridecagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Tetradecagon&>{}(&Tridecagon::join),
			resolve<Shape*, const Circle&>{}(&Tridecagon::join),
			resolve<Shape*, const Henagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Digon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Triangle&>{}(&Tetradecagon::join),
			resolve<Shape*, const Parallelogram&>{}(&Tetradecagon::join),
			resolve<Shape*, const Rhombus&>{}(&Tetradecagon::join),
			resolve<Shape*, const Rectangle&>{}(&Tetradecagon::join),
			resolve<Shape*, const Square&>{}(&Tetradecagon::join),
			resolve<Shape*, const Trapezoid&>{}(&Tetradecagon::join),
			resolve<Shape*, const Pentagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Hexagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Hexapod&>{}(&Tetradecagon::join),
			resolve<Shape*, const Heptagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Octagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Nonagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Decagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Hendecagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Dodecagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Tridecagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Tetradecagon&>{}(&Tetradecagon::join),
			resolve<Shape*, const Circle&>{}(&Tetradecagon::join),
			resolve<Shape*, const Henagon&>{}(&Circle::join),
			resolve<Shape*, const Digon&>{}(&Circle::join),
			resolve<Shape*, const Triangle&>{}(&Circle::join),
			resolve<Shape*, const Parallelogram&>{}(&Circle::join),
			resolve<Shape*, const Rhombus&>{}(&Circle::join),
			resolve<Shape*, const Rectangle&>{}(&Circle::join),
			resolve<Shape*, const Square&>{}(&Circle::join),
			resolve<Shape*, const Trapezoid&>{}(&Circle::join),
			resolve<Shape*, const Pentagon&>{}(&Circle::join),
			resolve<Shape*, const Hexagon&>{}(&Circle::join),
			resolve<Shape*, const Hexapod&>{}(&Circle::join),
			resolve<Shape*, const Heptagon&>{}(&Circle::join),
			resolve<Shape*, const Octagon&>{}(&Circle::join),
			resolve<Shape*, const Nonagon&>{}(&Circle::join),
			resolve<Shape*, const Decagon&>{}(&Circle::join),
			resolve<Shape*, const Hendecagon&>{}(&Circle::join),
			resolve<Shape*, const Dodecagon&>{}(&Circle::join),
			resolve<Shape*, const Tridecagon&>{}(&Circle::join),
			resolve<Shape*, const Tetradecagon&>{}(&Circle::join),
			resolve<Shape*, const Circle&>{}(&Circle::join) > ::call(*this, shape);
	}

если вы таки думаете что я написал это от руки то ніт ))

ЗЫ: оно даже не влезло в отложенные комментарию 15,000 символов

Ні, ви не вірно зрозуміли. Список міститиме лише 40 визначених функцій.

то продемонструйте щоб було зрозуміло хіба не у тому суть статті щоб було зрозуміло

Для ієрархії 20->4->1 класів визначено 40 функцій:

size_t add(const Base& a, const Base&b) {
	return multimethod<size_t,
		addi<Class<24>,Class< 6>>,
		addi<Class<24>,Class< 5>>,
		addi<Class<24>,Class< 4>>,
		addi<Class<22>,Class<22>>,
		addi<Class<22>,Class< 3>>,
		addi<Class<22>,Class< 2>>,
		addi<Class<20>,Class<22>>,
		addi<Class<20>,Class<12>>,
		addi<Class<21>,Class<15>>,
		addi<Class<21>,Class<14>>,
		addi<Class<20>,Class<13>>,
		addi<Class<20>,Class<12>>,
		addi<Class<19>,Class<16>>,
		addi<Class<18>,Class<15>>,
		addi<Class<18>,Class<14>>,
		addi<Class<17>,Class<13>>,
		addi<Class<17>,Class<12>>,
		addi<Class<16>,Class<11>>,
		addi<Class<16>,Class<10>>,
		addi<Class<15>,Class<15>>,
		addi<Class<14>,Class<14>>,
		addi<Class<14>,Class<13>>,
		addi<Class<13>,Class<13>>,
		addi<Class<12>,Class<12>>,
		addi<Class<11>,Class<11>>,
		addi<Class<10>,Class< 4>>,
		addi<Class< 9>,Class< 3>>,
		addi<Class< 9>,Class< 3>>,
		addi<Class< 8>,Class< 2>>,
		addi<Class< 8>,Class< 2>>,
		addi<Class< 7>,Class<11>>,
		addi<Class< 7>,Class<12>>,
		addi<Class< 7>,Class<10>>,
		addi<Class< 6>,Class<16>>,
		addi<Class< 6>,Class< 6>>,
		addi<Class< 5>,Class<15>>,
		addi<Class< 5>,Class< 5>>,
		addi<Class< 5>,Class< 1>>,
		addi<Class< 4>,Class< 4>>,
		addi<Class< 4>,Class< 1>>,
		addi<Class< 3>,Class< 3>>,
		addi<Class< 2>,Class< 2>>,
		addi<Class< 1>,Class< 1>>
     >::dispatch(a, b);
}
При нагоді оновлю на гітхабі.
kcachegrind пише для цієі функції 545 Ir per call
open metods звісно мають кращий результат — там хитро ідентифікують класи (в нутрощах компілятора) так що instance_of — це 1 чи 2 інструкції і далі зразу виклик функції через спеціяльну таблицю. Тут теж можна покращити якщо поставити собі таку мету

ото вже діло ото вже круто!

ну то продовжимо вже конкретику то що то за всі такі magic numbers?

size_t add(const Base& a, const Base&b) {
	return multimethod<size_t,
		addi<Class< _24_ >,Class< _6_ >>,
...

і що означає

addi

раніше у статті як я розумію воно не було introduced

чи то є просто абстракція для іллюстрації щоб не писати справжні назві класів? але знову ж таки конструкція addi відрізняється від того що було наведено у статті тож я гадаю потребує продовження пояснень

ЗЫ: ваш код взагалі компілюється під msvcc чи то не бага а фіча і ви просто не ставили собі таку мету?

Так, це я зробив шаблони Class та addi щоб не писати купу класів та методів для тестування продуктивності. Вони майже нічим не відрізняються від тих що у статті.
msvcc не ставив за мету. компілюю з gcc та clang, опції -std=c++17 -pedantic, -Wall

Ось код:

template<unsigned N>
class Class : public Class<N/5> {
public:
  static constexpr auto classid = multimethods::class_hash<Class>();
  virtual ~Class() {}
protected:
  using base = Class<N/5>;
  virtual bool instance_of(const class_info& expected) const noexcept {
	return classinfo(*this) == expected or base::instance_of(expected);
  }
};

template<>
class Class<0> {
public:
  static constexpr auto classid = multimethods::class_hash<Class>();
  template<class Expected>
  bool instanceof() const noexcept {
	return instance_of(classinfo<Expected>());
  }
  virtual ~Class() {}
protected:
  virtual bool instance_of(const class_info& expected) const noexcept {
	return classinfo(*this) == expected;
  }
};

using Base = Class<0>;
size_t add(const Base& a, const Base& b);

template<class A, class B>
size_t addi(const A& a, const B& b) {
	DTRACE();
	return a.classid + b.classid;
}

і ще цікаво як виглядають самі класи

Для ієрархії 20->4->1 класів визначено 40 функцій:

за наведеному у гітхаб прикладу вони виглядають так

class Shape {    
public:    
  static constexpr auto classid = class_hash<Shape>();
  template<class Expected>
  bool instanceof() const noexcept {
    return instance_of(classinfo<Expected>());
  }
  Shape* intersect(const Shape&) const;
  Shape* join(const Shape&) const;
  virtual ~Shape() {}
protected:
  virtual bool instance_of(const class_info& expected) const noexcept {
    return classinfo(*this) == expected;
  }
};

class Circle;
class Rect;

class Rect : public Shape {
public:    
  static constexpr auto classid = class_hash<Rect>();
  Shape* intersect(const Circle&) const { TRACE(); return nullptr; }
  Rect* intersect(const Rect&)const { TRACE(); return nullptr; }
  Shape* join(const Circle&) { TRACE(); return nullptr; }
  Shape* join(const Rect&) { TRACE(); return nullptr; }
protected:
  bool instance_of(const class_info& expected) const noexcept override {
    return classinfo(*this) == expected or Shape::instance_of(expected);
  }
};

class Circle : public Shape {
public:    
  static constexpr auto classid = class_hash<Circle>();
  Shape* intersect(const Circle&) const { TRACE(); return nullptr; }
  Shape* intersect(const Rect&) const { TRACE(); return nullptr; }
  Shape* join(const Circle&) { TRACE(); return nullptr; }
  Shape* join(const Rect&) { TRACE(); return nullptr; }
protected:
  bool instance_of(const class_info& expected) const noexcept override {
    return classinfo(*this) == expected or Shape::instance_of(expected);
  }
};    

struct Square : public Rect {
  static constexpr auto classid = class_hash<Square>();
protected:
  bool instance_of(const class_info& expected) const noexcept override {
    return classinfo(*this) == expected or Rect::instance_of(expected);
  }
};

при цьому методи Shape::intersect та Shape::join можна проігнорувати вони просто роблять перехід через this і відповідно можуть бути замінено функцією з +1 параметром на заміну this

як виглядають ці класи покладені на 40 функцій?

т.е. по факту диспетчеризация собирается в такую штуку

inline Shape* Shape::join(const Shape& shape) const {
  return multimethod<Shape*,
    resolve<Shape*, const Circle&>{}(&Rect::join),
    resolve<Shape*, const Rect&>{}(&Rect::join),
    resolve<Shape*, const Circle&>{}(&Circle::join),
    resolve<Shape*, const Rect&>{}(&Circle::join)>::call(*this, shape);    
}

=>

inline Shape* Shape::join(const Shape& shape) const {
  if ( typeof(*this) == Rect && typeof(shape) == Circle )
    return (Rect*)(this) -> join ( (Circle&) shape );
  else if ( typeof(*this) == Rect && typeof(shape) == Rect )
    return (Rect*)(this) -> join ( (Rect&) shape );
  ...
  else
    throw std::logic_error("Dispatcher failure");
}

+ шаблонная магия и constexpr дают всему этому возможность разворачиваться статически на случай если оба типа известны на этапе компиляции на прямую до

Rect rect;
Circle circle;

Shape & shape1 = rect;
Shape & shape2 = circle;

shape1.join(shape2); => rect.join(circle);

это то об чём идёт речь в

У прикладах, викладених на godbolt.org компілятор gcc генерує оптимізований код із статичною диспетчеризацією. Щоб була задіяна, динамічна диспетчеризація слід рознести test функції прикладів по різних одиницях компіляції.

gcc-8 з оптимізацією -O2 без RTTI генерує код у якому на рядок у таблиці виконується 7-9 інструкцій, влючно з call instance_of

1,000,000 диспетчеризацій по таблиці із 40 функцій виконується за 55 сек. Тобто, в середньому, 55 мкс на один виклик. Як поміряти cycles per loop щоб порівняти з [2] поки що не знайшов. Це чесна динамічна диспетчиризація. Статична унеможливлена рознесенням функцій по різних одиницях компіляції.

Почитал статью. Почитал переписку ниже. Теперь мне стало понятно, почему ембедеры так С++ не любят. И кстати на десктопе С++ и в вебе современный уже не жилец. Дальше его количество будет только уменьшаться.
И как ни странно, но его в большой степени может заменить раст и го.
Ниша С++ (большей частью С с классами) останется только в некоторых либах.

На десктопе C++ ещё какой жилец, и ещё лет 5-10 точно будет. Может и больше. Конкуренты хоть и есть, и часть рынка они откусили, но полностью плюсы так и не вытеснили.

Не понимаю я всех этих разговоров про «нишу». Говорят, мол, «ниши нет» — а работа есть, проектов хватает, вакансии открыты. Странно как-то получается.
Может, просто ниша более узкая, чем у других языков/технологий, но она таки есть?

А давай серьезно. Напиши ниши С++ на десктопе. Тогда и можно будет обсуждать, а не только тупо кидаться какашками друг в друга.
Сразу скажу, что либы с математикой, что вытягивают всё из железа большей частью на С (иногда С с классами). А обертки для удобного юзания их сейчас уже массово на Питоне, начинают появляться для раста, юлии.

По второму абзацу. За последние 20 лет ниши С++ постоянно уменьшались и в количестве и в размере.

Я не кидаюсь ни в кого какашками. Неужели мой предыдущий комментарий прозвучал настолько агрессивно? Я в него точно такой смысл не закладывал.
«Ниши» C++ я не напишу, потому что этот комментарий как раз и был о самом понятии «ниша». Я считаю, что это понятие не настолько однозначное, чтобы утверждать, есть у какого-то языка/технологии «ниша» или её нет, на уровне true/false.
Если работа и проекты есть — значит и ниша тоже есть. В какой-то степени. Где-то большей, где-то меньшей.

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

Извини. Я просто уже привык на местных форумах предупреждать о таком, если действительно интересно поговорить серьезно.

Если работа и проекты есть — значит и ниша тоже есть. В какой-то степени. Где-то большей, где-то меньшей.

Не логично. Вакансии на Коболе и Фортране до сих пор встречаются.

Я больше о том, действительно-ли у С++ остались ниши на десктопе. Одну я привел. Еще одна — драйвера и разные другие либы (для сети еще чего подобного). Но и в этих предпочитают С или С с классами.
И что мне сильно не нравиться в развитии С++ — это то, что его начинают превращать в что-то для всего вообще и сразу и похоже на java. С++ всё дальше уходить от идеи С. Простой язык, на котором легко достукиваться к железу с минимальными накладными.

Вакансии на Коболе и Фортране до сих пор встречаются.

Я тоже о них вспомнил. Разве это нельзя назвать «нишей»? Хз, думаю мы просто по-разному определяем это понятие.

Насчёт развития C++ — дело личных предпочтений. Как по мне, идея C++ изначально состояла как раз в этом (давать возможность достукиваться к железу, как в C, но при этом позволять строить вокруг этого высокоуровневые абстракции и писать некоторую часть кода «как на джаве») — современные навороты выглядят её закономерным продолжением.

Я тоже о них вспомнил. Разве это нельзя назвать «нишей»? Хз, думаю мы просто по-разному определяем это понятие.

Я бы не называл, если нужен один раз в 10 лет на весь мир для саппорта древних говен мамонта. Более того, если этот один очень дорогой или не найдут, то просто перепишут на чем-то новом толпой молодых.

Насчёт развития C++ — дело личных предпочтений.

Ну да, здесь нам уже спорить не о чем. Мы просто одно и тоже видим по разному и не сойдемся во взглядах. Тебе нравится путь развития С++, мне он не нравится. Это как спорить о вкусе устриц — кому-то нравятся, а кому-то нет.
Но, согласись, С++ уже совсем не простой язык, каким есть С или даже С с классами.

Но, согласись, С++ уже совсем не простой язык, каким есть С или даже С с классами.

а что, в с++20 совместимость с С99 уже поломали?

А это вопрос к чему? Мы сравниваем С и С++, а не компиляторы и подмножества С++. Кстати, я не уверен, что C++ и С99 совместимы на 100%.

к тому, что никто тебя не заставляет использовать все новые фичи с++

И где я что-то подобное написал?

Её там отродясь не было, даже между C99 и C++98.

Язык C никогда не являлся подмножеством C++.

Например, куча ключевых слов из плюсов («class» и т.д.) могут быть свободно использованы в качестве идентификаторов в сях и там это будет норм, а в плюсах не скомпилится.
Ещё в сях есть такая интересная вещь как VLA, которой нет в плюсах (и слава богу, имхо).
Символьные литералы в плюсах имеют тип char, а в сях int.
Пустые круглые скобки в объявлении функции в плюсах означают отсутствие параметров, а в сях — «хз, сколько там параметров, узнаешь потом, когда наткнёшься на определение».

Это так, первое, что пришло в голову.

Её там отродясь не было, даже между C99 и C++98.

собсно я имел ввиду последний с++ стандарт перед с++11
прасцыте

Перед 11 был 03, до него только 98.
Раньше язык вообще не был стандартизирован и мог отличаться от компилятора к компилятору.
Но 100% совместимости с Си у него не было никогда. Хоть и большая часть сишного кода таки скомпилится в C++ и, вероятно, даже будет правильно работать.

Ещё в сях есть такая интересная вещь как VLA, которой нет в плюсах (и слава богу, имхо).

Поэтому программеры на плюсах мудохаются с alloca() c теми же последствиями.

Интересное наблюдение, несмотря на VLA программеры на C все же бережнее обращаются со стеком в отличии от программеров на С++, которые создают объекты на стеке и даже не представляют сколько они весят.

в отличии от программеров на С++, которые создают объекты на стеке и даже не представляют сколько они весят.

либо не вдупляют создаются ли там объекты вообще ))

Либо не умеют завязывать себе шнурки, спотыкаются по дороге на работу, падают в канаву и умирают.

ну да )) в плюсовой реализации утрируя как результат плюсовый код по производительности на уровне джавы и местами даже отстаёт и потому что по ходу исполнения почему-то надо постоянно «завязывать шнурки» ))

Именно. Таким людям я бы советовал переходить на джаву и не издеваться ни над собой, ни над коллегами.
Большинство же из тех, кто остаются на плюсах и дорастают хотя бы до уровня миддла, как минимум способны понимать, где у них создаются объекты, а где нет.

Кстати, наткнулся на забавный пример с производительностью. Нужно в один список запихать инстансы разных подклассов.

Решение на С:

struct Node {
    struct Node* next;
    struct Node* prev;
    enum type    type;
};
подклассы делаются по типу
struct ChildClass {
    strcut Node   node;
    strcut MyData data
};
и при помощи кастов и какой-то матери все спокойно ложится в один список
struct Node* list;

Решение на С++:

class Parent;
std::list<Parent*> list;

То есть, чтобы добраться до данных в С++ будет в два раза больше переходов по указателям. Есть другие варианты решения? За касты с С++ проекта могут выгнать.

PS придумал. В парента можно вписать указатели листа, но тогда это будет не std::list list; а Parent* list;

не способность stl работать на прямую с классическими иерархиями и полиморфизмом к.м.к. отдельная попоболь всей системы templates вообще какими они есть в «плюсах» читай прямо templates и «плюсы» это вообще две разных парадигмы причём крайне трудно плохо пересекающиеся

... и кстати отсюда вся петрушка с сабжевой задачей ))

То есть, чтобы добраться до данных в С++ будет в два раза больше переходов по указателям.

почему?

Потому что лист содержит Parent*. Первый переход по указателю на следующую ноду, второй — по указателю из ноды на объект.
В С объект содержится в ноде по значению

struct ChildClass {
    strcut Node   node;
    strcut MyData data
};
и когда нода подтянулась в кеш, объект подтянулся вместе с ней. То есть — только один переход по указателю на ноду.

а если

std::list<Parent> list;

?

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

1) Судя по наличию operator=() variant как юнион имеет размер максимального типа, который может содержать. Если один из типов большой — будет стремно. Потом, у него будет проблемы с variable-length arrays вроде:

strcut TLV {
    uint8_t type;
    uint8_t length;
    uint8_t value[];
};
2) Нужно в параметры темплейта прописать все возможные значения, а это может быть нереальной длины список.

Пояснение где оно попалось: список объектов асинхронных сценариев, которые в данный момент обрабатывает модуль. Так как сценариев может быть много разных, то в один список нужно класть разные типы.
Вариант 2, более злой: очередь сообщений. Вот тут и получится нереальное количество параметров темплейта, очень разные размеры, массивы переменной длины, и нужна очень шустрая работа. У меня решено интрузивным списком:

class Message {
	friend class IntrusiveList<Message>;
	DISALLOW_COPY_AND_ASSIGN(Message);

public:
	virtual ~Message() {}

	virtual void Dispatch(Handler& handler) const = 0;

protected:
	Message(const Cookie id) : id_(id), prev_(NULL), next_(NULL) {}

	const Cookie	id_;

	Message*	prev_;
	Message* 	next_;
};
Но это никак не С++ стайл и никак не стд.
Но это никак не С++ стайл и никак не стд.

эээ. а ктото заставляет?

Никто)
Но тут где-то говорили о скорости C vs C++. Подвернулась интересная задача, которая в C++ style не делается эффективно.

стдлиб не совсем с++ стиль. это просто библиотека
всегда можно сделать свою реализацию

Поддерживаю.

Многие забывают, что стандартная библиотека C++ — не истина в последней инстанции. Её задача — предоставить достаточно хорошие решения для некоторых наиболее популярных сценариев. «Достаточно хорошие», которые устроят в большинстве случаев, когда текущее место кода не является боттлнеком по производительности или потреблению памяти. Но они далеко не всегда идеальные.

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

Присобачь умные указатели и будет красивый С++ стайл. И с блэкджеком и со шлюхами, как хотели и без накладных.

У умного указателя будет тип smart_ptr и smart_ptr. Их нельзя в один список положить.

Не только. Их сейчас стадо в STL, во вторых есть уже и вектора умных указателей. В третьих можешь сам под свою ситуацию написать — это не сложно.
В четвертых, если кодеры не бестолочи, то и с просто указателем справятся и динамик кастами.
Ну и умный указатель сходу скрещивается с виртуальностью автоматически.

Сорри, тут ДОУ порезал темплейты. Было задумано так:

smart_ptr<Derived1> vs smart_ptr<Derived2>
в результате типы двух указателей разные, так как внутри разные классы как параметр темплейта

Я не вникал особо. Но пронаследуй обеих от Вазе некоего и потом динамик кастом к нужному тебе. Не забудь про виртуальный деструктор и всё будет хорошо.

Так умный указатель — это переход по указателю. Оно будет тормознее, чем на С с данными напрямую в списке. Об этом же и весь вопрос.

Так ты же хотел красиво и все по заветам С++.
Второе, переход по указателю не всегда дорог относительно остального кода. Так что часто лучше сделать красиво.
Ну и как только у тебя появилась виртуальность у тебя уже будет переход по указателю, а вот смартпоинтер компилятор часто может скомпилировать с нулевыми накладными.

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

А еще типично копирование объектов добавиться. Ну а виртуальностью будет жопа, про которую ты написал.

Денис, а не проще ли будет иногда не искать (когда слишком долго) а создать заново? При условии, что сам идентификатор, будет содержать все необходимые правила для создания.
То есть: берем исходный материал (в котором будем искать) и берем некое айди (оно составное) Применяем к материалу поочередно в любом порядке части айди как функции отсева/отбора и в результате получаем область — конечный материал — содержащий много вероятно искомое. Последовательная фильтрация такая, но она очень похожа не на поиск, а на создание максимально похожего на искомое. Вероятностный поиск.

Это ты описал как устроена хеш таблица

Мне сейчас нужна вот такая структура данных. dou.ua/...​rums/topic/27032/#2048191
Не хочу использовать РБД, может это просто список из каких-то объектов?

За касты с С++ проекта могут выгнать.

Вот в этом радикализме корень всего зла. А не в плюсах как таковых.
Просто многие люди не понимают, что плюсы — мультипарадигменный язык, и для одних задач лучше подходит одна парадигма, для других — другая. Вместо этого подгоняют всё под своё представление про «единственные правильные» плюсы, в которых нельзя кастить.

ЗЫ
Вспомнился Qt, где у QEvent’а есть поле type, которое можно проверить на соответствие нужному значению из енума и, если соответствует, с чистой совестью статик_ (даже не динамик_!) _кастить к конкретному производному типу.
И это считается вполне себе нормальной практикой.

Вот сейчас окажется, что у него еще есть поля prev и next, или он создается прямо в ноде списка)

Поэтому программеры на плюсах мудохаются с alloca() c теми же последствиями.

И это замечательно, я считаю.
Потому что когда ты делаешь стрёмную фигню — ты должен отдавать себе отчёт, что это стрёмная фигня. И код должен выглядеть соответствующе. Чтоб, пока его печатаешь, ты несколько раз подумал, действительно ли это нужно, и вообще не расслаблялся. Не зря оператор reinterpret_cast такой длинный и неудобный для набора :)
А вот когда код с обычным массивом и код с VLA выглядят практически одинаково, но генерируют совершенно разный асм и работают по-разному в плане перформанса (не говоря уже о риске переполнить стек) — это, я считаю, не очень прикольно.

Кстати, в плюсах есть альтернативы alloca(). Мне, например, во многих случаях, где аллокация «динамической» памяти на стеке действительно имела смысл, помогали контейнеры типа small_vector или static_vector. C++17 также вводит pmr для подобных целей.

Интересное наблюдение, несмотря на VLA программеры на C все же бережнее обращаются со стеком в отличии от программеров на С++, которые создают объекты на стеке и даже не представляют сколько они весят.

А вот это уже манипуляция. Бережность использования стека зависит не от языка, а от таргет платформ в частности и направления разработки в целом. Да: эмбеддеде уважающий себя программист будет обращаться со стеком более бережно, чем в десктопе. Ибо в эмбеддеде стек как правило гораздо более ограничен, в то время как в десктопе его переполнить ещё нужно постараться.
И если в эмбеддеде преобладает сишка, то у кого-то могут напроситься такие выводы. Но не надо путать, что здесь является причиной, а что следствием.

Потому что когда ты делаешь стрёмную фигню — ты должен отдавать себе отчёт, что это стрёмная фигня.

И при этом создавать целые объекты на стеке и брать указатели и отдавать другим классам на объекты в стеке и т.п. Какое-то странное ограничение не наступать на грабли N7 из 100500 доступных граблей.

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

Не надо сюда эмбеддед приплетать. Я говорю о языках. Например, Intel Poland в Graphics Compute Runtime проекте на стеке создают мегабайты, они разворачивают LLVM там же. А на вопрос а сколько весит ваш объект в стеке — да он маленький, фигню весит. 730Kb o_O для временного объекта, потому что наследование, множественное наследование никто не отменял, разработчик топ класса даже не представляет какого свинского размера объект.

А стек — это не бесплатно, в ОС которых пекутся про стабильность, ставят гарды на стек. Весь стек в большинстве ОС lazy mapping который будет трапать ядро при первом доступе к странице стека и который освобождаться не будет до смерти процесса. Это может быть многопоточная программа, где в каждом потоке свой стек и стек в потоках лимитирован, он не растёт «бесконечно».

Так что это именно язык, который прячет размеры объектов от разработчика.

создавать целые объекты на стеке

Как громкая фраза в дискуссии без конкретных примеров — аргумент хороший. Но на практике «создание целого объекта» во многих случаях оптимизируется компилятором до такого асма, который получил бы сишник при аккуратном написании кода без «объектов».

Когда же оптимизаторы не справляются — да, это проблема.
И в таких случаях действительно справедливо будет заявить, что чистая сишка будет лучше в плане перформанса (скорость разработки и удобство сопровождения пока что оставим за скобками).

К счастью, возможности оптимизаторов за последние 10 лет сделали большой скачок вперёд и продолжают развиваться в современных компиляторах. Так что чем дальше, тем меньше таких случаев будет оставаться.

Какое-то странное ограничение не наступать на грабли N7 из 100500 доступных граблей.

Хоть что-то.
Я бы предпочёл, чтобы в плюсах было больше таких вот ограничений. Но имеем что имеем.

Не надо сюда эмбеддед приплетать.

Разве в эмбеддеде тоже на стеке можно создавать дофига объектов, как в десктопе, не рискуя при этом поймать переполнение? Я думал, что по крайней мере на некоторых девайсах размер стека сильно ограничен и там приходится уделять выделяемой на нём памяти значительно большее внимание, чем при программировании под десктопы.

Я говорю о языках.

Значит ты неправ. Я плюсовик и я знаю, когда и какие объекты у меня создаются. Про их размеры тоже не забываю. В том числе знаю, какие накладные расходы несёт в себе множественное наследование. И я явно не один такой.

Это не вина языка, что некоторые люди этими наследованиями злоупотребляют. Как говорил один известный плюсовик, «inheritance is the base class of evil».

Так что это именно язык, который прячет размеры объектов от разработчика.

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

Как громкая фраза в дискуссии без конкретных примеров — аргумент хороший. Но на практике «создание целого объекта» во многих случаях оптимизируется компилятором до такого асма, который получил бы сишник при аккуратном написании кода без «объектов».

Тоже самое без конкретных примеров, вера в оптимизатор хуже веры в бога. Как он оптимизирует инициализацию в закрытых конструкторах базовых классов?

Разве в эмбеддеде тоже на стеке можно создавать дофига объектов, как в десктопе, не рискуя при этом поймать переполнение? Я думал, что по крайней мере на некоторых девайсах размер стека сильно ограничен и там приходится уделять выделяемой на нём памяти значительно большее внимание, чем при программировании под десктопы.

Причём тут дивайсы? Например в luxury сегменте в automotive могут стоять такие дивайсы, что не у каждого такой десктоп есть. Вопрос не в дивайсах, а в области применения и требований к ним.

А вообще ты прекрасно показал что ты над этим тоже не задумываешься, std::thread используют системные дефолтовые размеры стека для потоков. А размеры стека потоков не растут, потому что эти стеки это обычная выделенная память и расположенная в виртуальном адресном пространстве процесса (не как стек главного потока/процесса), там маленький reserved space и обычно половина его committed space сразу. И размер стека в потоках не безграничный. Можно даже сказать что ты не знаешь дефолтовых значений в линуксе и в винде, тот, кто хоть раз задумывался о размерах знают о конечной размерности.

Значит ты неправ. Я плюсовик и я знаю, когда и какие объекты у меня создаются. Про их размеры тоже не забываю.

Как ты контролируешь размеры объектов и как ты про них не забываешь? o_O Ну вот ты создал на стеке объект на 0.5Mb и думаешь, что это вполне нормально, вроде бы не много, а тот кто создаст или будет использовать твой класс тоже создаст объект на 200Кб — вроде бы мелочь, а тот ещё и такая матрёшка лопнет очень скоро.

Ибо в C можно сделать ровно то же самое — просто замени наследование на композицию и классы на структуры.

Оно перестанет иметь читаемый вид после пары вложений. Там естественные препятствия к созданию подобных структур. Пересечение имён, отсутствие private даст о себе знать очень скоро или придётся делать многуровневые вложение, которые никто в голове не сможет удержать.

вера в оптимизатор хуже веры в бога

Согласен. Поэтому я смотрю на генерируемый асм и меряю производительность профайлером.

А вообще ты прекрасно показал что ты над этим тоже не задумываешься

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

Я сказал, что во всех задачах, где мне приходилось использовать std::thread, мне хватало дефолтного размера стека. Ибо я не создаю там объектов по 730 килобайт, как некоторые. В коде, с которым я работаю, размеры объектов на порядок меньше. И слава богу.
Не спорю, что если бы каким-то чудом у меня в коде получился класс, объект которого будет занимать столько памяти, и я попытался бы его создать на стеке, на десктопе тоже может случиться бо-бо.
Особенно если речь идёт про не-главные потоки приложения.
Я помню, что их стек бывает сильно ограничен, ты сам мне об этом говорил здесь на ДОУ дофига лет назад :)

Как ты контролируешь размеры объектов и как ты про них не забываешь?

Зависит от контекста. В целом держу в голове размеры примитивных типов и базовых классов стандартной библиотеки, а также понимаю, сколько места будут занимать структуры с их комбинациями внутри (с учётом паддингов). Например, когда я передаю какую-то лямбду в качестве коллбека, сохраняемого в нашем самописном аналоге std::function — я стараюсь минимизировать её захватываемые поля так, чтобы их суммарный размер не превышал 24 байта. Поскольку именно до такого размера наш function разместит всё внутри объекта, не прибегая к динамическим аллокациям.
Когда речь идёт о сериализации, тоже смотрю, сколько и чего я пишу, чтоб не отожрать больше места, чем нужно.
Универсального ответа здесь нет, как именно я это контролирую. Но определённо я не пускаю дела на самотёк в духе «давай здесь проаллоцируем прямо на стеке 100500 объектов типа X, места точно хватит, стек ведь резиновый».

Ну вот ты создал на стеке объект на 0.5Mb и думаешь, что это вполне нормально

А вот такого я точно не делаю.
Конкретно на текущем проекте таких больших классов, к счастью, нет и близко. Не представляю, как мы должны «допрограммироваться», чтобы они появились.
А если бы даже такие и были, я бы не подумал, что создавать их на стеке — нормально. Предпочту их аллоцировать в куче, раз уж вылезла необходимость их вообще создавать.

Оно перестанет иметь читаемый вид после пары вложений. Там естественные препятствия к созданию подобных структур. Пересечение имён, отсутствие private даст о себе знать очень скоро или придётся делать многуровневые вложение, которые никто в голове не сможет удержать.

Ну не знаю. Никто ведь не мешает реализовать множество функций, которые будут по принципу матрёшки отшелушивать по одному уровню вложенности передавать указатели на «подобъекты» в следующие функции. Тогда сложность будет тоже вполне себе «скрытой» от программиста, если он будет смотреть на реализацию отдельных функций, а не на архитектуру в целом.
Если же смотреть на архитектуру в целом, то и с плюсами станет заметно, где класс раздули.

Как ты контролируешь размеры объектов и как ты про них не забываешь? o_O Ну вот ты создал на стеке объект на 0.5Mb и думаешь, что это вполне нормально, вроде бы не много, а тот кто создаст или будет использовать твой класс тоже создаст объект на 200Кб — вроде бы мелочь, а тот ещё и такая матрёшка лопнет очень скоро.

отчасти поэтому в Qt классах вовсю используются d-pointers

Не только Qt.
Мы вот тоже композицию как правило делаем через unique_ptr на «подобъект», если тот достаточно большой. И если нет серьёзной причины поступать иначе (например, проблемы с перформансом или фрагментацей памяти).
Я думаю, это почти везде нормальная практика.

С наследованием, правда, так не сделаешь — ну дык и нефиг им настолько злоупотреблять. На каждом заборе написано уже давно: «favor composition over inheritance».

У компилятора есть предупреждение когда создаются большие объекты на стеке.

Кстати, в плюсах есть альтернативы alloca(). Мне, например, во многих случаях, где аллокация «динамической» памяти на стеке действительно имела смысл, помогали контейнеры типа small_vector или static_vector. C++17 также вводит pmr для подобных целей.

круто )) ты точно уверен что здесь ты просто не расписал что просто сам не знаешь точно что куда совать?

А вот это уже манипуляция. Бережность использования стека зависит не от языка, а от таргет платформ в частности и направления разработки в целом. Да: эмбеддеде уважающий себя программист будет обращаться со стеком более бережно, чем в десктопе. Ибо в эмбеддеде стек как правило гораздо более ограничен, в то время как в десктопе его переполнить ещё нужно постараться.

одним из любимых моих вопросов на понимание многопоточности стал «прикиньте на пальцах с потолка сколько всего потоков можно открыть на конкретной системе вот стоит железяка так сколько?» ))

вот на пример классика «плюсовики создают объекты на стеке но на самом деле не представляют сколько весит само создание конкретного объекта»

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

ок я признаю я был не прав и я зря встрял

ты точно уверен что здесь ты просто не расписал что просто сам не знаешь точно что куда совать?

Да, я точно уверен, что я такого не расписал. А ты точно уверен, что твои претензии ко мне имеют что-то общее с тем, о чём шла речь выше?

а что, в с++20 совместимость с С99 уже поломали?

Как можно поломать то, чего не было? o_O

А давай серьезно. Напиши ниши С++ на десктопе. Тогда и можно будет обсуждать, а не только тупо кидаться какашками друг в друга.

Теоретически, приложения написанные на Qt это ниша — т.к. компиляторы под виндой и линуксом умеют в новые стандарты «плюсов», a Qt пользуют для портабельности.

А учитывая, что линуксоиды всё одно денег не платят — экономическая целесообразность написания десктоп-приложений для них сомнительна. Во всяком случае, концерны заточенные на серьёзную разработку — таким не занимаются и последние лет 10, пишут декстоп на «шарпах» и только под винду.

Теоретически, приложения написанные на Qt это ниша — т.к. компиляторы под виндой и линуксом умеют в новые стандарты «плюсов», a Qt пользуют для портабельности.

Кроме QtCreator я относительно новых приложений на Qt не видел. Чаще электрон юзают и не возятся с плюсами для гуев.

Во всяком случае, концерны заточенные на серьёзную разработку — таким не занимаются и последние лет 10, пишут декстоп на «шарпах» и только под винду.

Сейчас и от шарпа избавляются переходят на жабаскрипт, чтобы один код и для веба и для десктопа.

Датасаенс и машинленинг весь на питон перешел и жабу. На С или С++ остаются только самые глубокие внутренности (где таки нужно из железа максимум вытянуть).

Кроме QtCreator я относительно новых приложений на Qt не видел

KDE :)

Понятно, почему не видел. Не пользуюсь KDE.

Тут скорее идет речь о смерти и перерождении десктопа. Терминал к меинфрейму -> персональный компутер -> браузер (терминал) к облако-серверу (меинфрейму). Новый цикл децентрализации начнется вот-вот

линуксоиды всё одно денег не платят

Ты забыл про ембеддед лялекс

В нас на Qt гуя. Кросплатформна, плюс андроїд. Може й під айфон, лінь збирати.

В сусідів з Дельфі мігрують на Qt.

Швидка, як блискавка. Гостра, як бритва.

Що ми робимо не так?

Яка в 2003-му році була розумна альтернатива? VC++ 7.0?

Швидкість розробки нижча.

Якість на Дельфі можна (було) давати дуже непогану. Все залежить від команди.

Формошльопати формочки нижча.
Але якщо зробити SDK для графіки, то уже нє.

Ми ніколи не формошльопили.

Я мав на увазі — ми з кумом

Всі так кажуть, хто ж зізнається

Ниша С++

легасі код, шото Кобола

Кобола

кабалла жеж

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

Підтримувати «класичну» подвійну диспетчеризацію теж не мед. Запропоноване рішення може і виглядає складно, проте, на мою думку, простіше у використанні і підтримці.

Що таке «класична»? Чим вона ліпша за звичайну матрицю операторів?

Я так розумію, мова йшла ось про це: en.wikipedia.org/...​ch#Double_dispatch_in_C

Підтримувати «класичну» подвійну диспетчеризацію теж не мед.

Не мед, але й не ложка дьогтю.
У класичної подвійної диспетчеризації перевага в тому, що її розуміє будь-який програміст, навіть джун. В той час як наведене у статті рішення вимагає від девелоперів добре розбиратися у темплейтах і деяких інших нюансах C++, включаючи нововведення C++17, в яких навіть не кожен senior розбирається, тому що на практиці воно потрібно надто рідко.

Люто плюсую.

Я хоть сам и люблю подобные темплейтовелосипеды, но вот например в моей нынешней команде таких же задротов ещё человека полтора — остальные просто кодят на «нормальном» C++ без подобных извращений и им такое суппортить вообще нафиг не упёрлось. На предыдущих работах концентрация темплейтозадротов тоже была не большей.
Задротство задротством, но у хорошего программиста интересы бизнеса и команды должны иметь более высокий приоритет.

По опыту работы я заметил, что как кто-то начинает так играться с С++ (пусть даже это гениально удобно) от него на конторе избавляются. Основная масса людей предпочитает попроще и по понятнее. Сам уже давно пишу на С с классами большей частью. Такой код может прочитать любой, кто хоть чуть умеет С++.
И самое смешное, что простой код компиляторы лучше оптимизируют. Пишешь тупо и однозначно и компилятор применит давно отлаженную конструкцию. Пишешь чуть сложнее и компилятор тупо транслирует.

как кто-то начинает так играться с С++ (пусть даже это гениально удобно) от него на конторе избавляются

Ну, от меня до сих пор не избавились. Хотя «играться» я раз в энное время тоже могу. Здесь вопрос в качестве «игры». Я как уже достаточно опытный для этих дел специалист понимаю, где играться стоит, а где нет, и какие будут преимущества и недостатки у наличия/отсутствия того или иного темплейтовелосипеда. При оценке, естественно, учитываю также фактор maintainability. И фидбеки у коллег спрашиваю.
Если подходить с умом, подобные «игры» не есть чем-то плохим. В редких случаях они оправданы. Плохо — когда ими злоупотребляют и лепят туда, где всё можно было бы сделать проще, ничего не потеряв.

Вот в этом и проблема.
Когда некий сеньер с 20 годами опыта пишет какой-то модуль с шаблонной магией и всем этим удобно пользоваться. С другой стороны в силу опыта он ошибок в коде делает 1 на 10000 строк, в отличие от джуна с 10 ошибками на 100 строк. То всё отлично.

Но большая проблема в том, что мидлы насмотревшись красоты шаблонной магии начинают ее лепить с 1 ошибкой на 100 строк их кода и после нужно другим мидлам это саппортить — в этом случае начинается жесть.

По этой причине я написал о потерянном абзаце в выводах статьи.

Я до сих пор не синьор и опыта у меня в разы меньше, но в последние 2-3 года у меня получается примерно так

пишет какой-то модуль с шаблонной магией и всем этим удобно пользоваться

Но может это я такой редкий покемон.

А вот насчёт этого

Но большая проблема в том, что мидлы насмотревшись красоты шаблонной магии начинают ее лепить с 1 ошибкой на 100 строк их кода и после нужно другим мидлам это саппортить — в этом случае начинается жесть.

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

Но может это я такой редкий покемон.

Да. Таких не много.

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

А ты рулил командой. Даже при налаженный процессах срачи на ревью на подобные полурелигиозные темы — это жесть. Поэтому большие конторы часто просто запрещают кучу фичей С++, чтобы избавиться от таких срачей и чтобы программисты писали простой и тупой код. На ревью срачи даже в более простых вещах иногда приходилось разрешать жестким приказом. Сделай как приказал или пойдешь на мороз. Человек же написал, постарался, считает, что сделал идеально, а тут критикуют и дальше начинается срач. И работа нескольких человек остановилась. Более того в такой срач завлекаются и другие из команды и вот они уже воюют, а не работают.

Нет, командой я не рулил.

Ну жесть, если такое происходит, что я тут ещё могу сказать. У нас такого пока не было. Какие-то дискуссии имели место, конечно, но надолго они не затягивались и люди всегда в конце приходили к взаимопониманию, а не продолжали сраться. Возможно, мне просто сильно повезло с командой.

Помнишь один советский фильм «У вас несчастные случаи на стройке были?»?

Да сейчас ты в команде, где у всех пока взгляды похожие. И у вас кто-то прочитает одно, кто-то другое и взгляды начнут расходится. А еще компиляторы тупые, не забывай. Мне в одном телеграм чатике один эмбедер недавно показал, как сливает современный компилятор gcc просто при юзании STL. Я был уверен на 100%, что на том простом коде компилятор не сольет. Для сравнения был аналогичный код на С.
А вот пример того, когда мне так приказывать пришлось. Чел обнулял члены класса через memset и this и был на 100% уверен в своей правоте.

Чел обнулял члены класса через memset и this и был на 100% уверен в своей правоте.

Ну, для POD это ещё кое-как можно допустить. Хоть и стрёмно, ибо завтра в эту структуру могут добавить какой-нибудь std::string, а обнуление переписать с мемсета на что-нибудь другое забудут.
Если же у класса были нетривиальные мемберы и/или указатель на vtable, то чел вообще некомпетентен.

Были.
Но после некоторого причесывания силовым методом подобного его код вполне работал и решал задачу. Ему нужно было написать навороченный и сложный конечный автомат по одному этапу разбора текста. Т.е. в теме он разобрался, написал рабочее, но в некоторых чисто програмерских вещах вот так бредил.
Вот именно твои аргументы он не воспринял.

А да, я еще написал большой кусок кода, что отлавливал исключения в толпе модулей, в том числе и SEH, отключал модуль из пайплайна и ругался в лог. После програмеры фиксили то, что им в лог написано было. В общем и в срок уложились и ТЗ полностью выполнили и даже больше сделали, чем по ТЗ нужно было (контора продуктовая).

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

код должен быть максимально тупой и простой и лучше без шаблонной магии.

істіну глаголіш

А вот пример того, когда мне так приказывать пришлось. Чел обнулял члены класса через memset и this и был на 100% уверен в своей правоте.

Я на первой работе так делал, и не мог понять, почему падает)

Логично, потому как нужно хорошо понимать, как классы и шаблоны в С++ устроены и что получается в результате компиляции.
Тебе еще повезло, что он падал. Он мог долго не падать — причин море.

И тут возникает странная проблема:
Код на C++ можно позволить писать только сеньорам?
А как получить сеньоров, если джунам и мидлам писать его нельзя, как им учиться?

Ну вот там оно падало странным образом (так как был вызов виртуального метода, оперативы мало, эмбедед — то вместо попадания в 0 вызывался адрес типа 60 или 80, и выполнение улетало в неведомую даль. Пошел к лиду, говорю, все написал, а оно че-то странно падает. Он глянул отладчик, и сразу угадал про мемсет. Вот для этого нужен лид.

У меня был подобный случай. В общем поехал я в головной офис. А тестеры у нас в головном сидели. Думаю, надо подойти поговорить. И вот одна тестерша показывает мне баг — падает. Грю покажи, как ругается. И дальше диктую ее, как баг описать «в таком то модуле пофиксить проблему в 2G памяти». Приезжаю обратно, мои ржут, грят — ты баг написал? Тестеры такое не способны написать, а ты был как раз там.
Пофиксили за полчаса.

З.Ы. Но ты то послушался, а многие будут спорить и аргументы выше не примут.

А удивился, а он рассказал, что такое vptr и где он лежит.

Если бы у тебя в классе том было что-то еще — объект с нетривиальным конструктором, какие указатели, то было бы еще веселее. Таблица функций — это только одна из причин, почему подобное делать нельзя.
Или что-то стороннее из железа.

Ну я до того в служебных классах не использовал наследование и не наткнулся. А без наследования — оно всегда прекрасно работало) Ружье стреляет в зависимости от фазы луны.

Добавь слово virtual — и веселье тебе с оным будет обеспечено с таким обнулением.
А с virtual для не очень опытного в С++ начинается жуткое с кастами — какой-когда.
А тут еще и шаблонная магия. Сколько раз хотелось извратиться, чтобы скрестить шаблоны с виртуальностью — бил себя по рукам за попытки такого. Типа сделать один вызов do_хорошо().

Код на C++ можно позволить писать только сеньорам?
А как получить сеньоров, если джунам и мидлам писать его нельзя, как им учиться?

А это зависит от того, кто как определяет понятия «джун», «миддл» и «синьор». Я бы отказывался от системы определений, в которой человек, понимающий модель памяти C++, время жизни объектов и прочие такие штуки, считается уже синьором.

И вот выше один пример сам признался, как в молодости на С++ фейлил. Я уже не помню про себя (давно было), но скорее всего подобных фейлов было море.
А еще добавим любимую фишку С++ — UВ.
И вот мы приходим к выводу, что на С++ в прод можно позволить писать только челам с 10 годами «честного» опыта в С++.

А еще добавим любимую фишку С++ — UВ.
И вот мы приходим к выводу, что на С++ в прод можно позволить писать только челам с 10 годами «честного» опыта в С++.

Если говорить про коммерческий опыт, то я где-то за 2 года пришёл к состоянию, о котором ты говоришь. Но я ещё до этого много книжек и статей читал про C++ и смотрел доклады с различных конференций. Ну и продолжал это делать параллельно с работой.
Можно списать это на мою редкую покемонистость, но вот про 10 лет, как по мне, это всё же перегиб. Разве что если человек принципиально не хочет читать хотя бы того же Майерса и Саттера-Александреску, предпочитая набивать шишки самостоятельно.

Вообще, я считаю, важно разделять два ортогональных набора навыков, входящие в состав скиллов программиста:
1) знание самого языка, понимание его модели памяти, владение «хорошими практиками», умение не допускать UB в новом коде и распознавать его в существующем, и т.д.;
2) способность пользоваться этим языком как инструментом для быстрого и качественного решения задач бизнеса, понимать сам бизнес, организацию процессов внутри команды, разбираться в архитектуре проекта и не допускать её засирания, уметь эффективно коммуницировать с программистами и представителями других ролей, и т.д.

Сейчас мы говорили исключительно о пункте (1). Который, как я считаю, хоть и требует бо́льших вложений по сравнению с другими языками программирования, но чем-то запредельным, требующим дофига опыта, не является: его можно прокачивать и дома, читая литературу, слушая доклады и играясь с компиляторами.

Как по мне, именно (2) отвечает за «синьорность» программиста. Это то, что приходит только с опытом и никак иначе не прокачивается.
Например я ещё когда был джуном шарил C++ не хуже местных синьоров (иногда даже они у меня что-то спрашивали, кек). Но кем-то выше джуна меня это не делало — пользы бизнесу я всё равно приносил как джун и не более того. Ибо не было нужных скиллов «вне C++».
А бывает и наоборот, когда человек реально синьор, просто с плюсами редко работает, или работает только с их небольшим подмножеством, или только на конкретной платформе, где UB не такой уж и «U». Меньшим синьором его это не делает. При желании разберётся. Или напишет костяк, а такие детали делегирует какому-то миддлу-энтузиасту вроде меня :)

Я понимаю твой оптимизм, но уже давно не разделяю.

В STL від GCC 2.95 був баг в класі std::string.

В результаті десь раз в годину бекенд падав.

Два місяці шукали. Коли знайшов і побачив, дуже погано думав про авторів оцього всього.

Падало приведення до (char *). Більшого трешу я не бачив. Хіба в QNX в мережевому стеку, але це до Горчака.

В STL від GCC 2.95 був баг в класі std::string.

в мене було на переході на нову студію почало чи то падати чи то не працювати з’ясувалося вони там у себе у нутрощах геть поміняли принцип роботи здається з std::variant і раніше зроблені припущення що до його використання стали вже не діючими ))

такі да віддебажити це не можливо гріб код шаблонів щоб зрозуміти хто за ким стояв і що вся ця . означає

ЗЫ: втім можливо воно просто перестало компілятися вже точно не пам’ятаю ))

ЗЫ: доречі сабжевий код не компіляється на віжуал студії ))

там ТОВ «ИРІЙ», і свій манямірок митців, а не «конвейєрна розробка»

О, флюман, який ображається на переходи на особистості, вирішив перейти на особистості.
Манямірок це у тебе з твоїми однодумцями, які хейтять плюси просто тому що хейтять.

До чого тут перехід на особистості?
Чи ти не розумієш відмінності вимог до персоналу і до коду при «масовому виробництві» та «поштучному»?

Що може собі дозволити «митець» в «творчій майстерні» (ліпнину із завитушками), на «конвейрі» не прийнятне.

флюман

і хто перейшов?

До чого тут перехід на особистості?
там ТОВ «ИРІЙ», і свій манямірок митців

І справді, чого це я.

Чи ти не розумієш відмінності вимог до персоналу і до коду при «масовому виробництві» та «поштучному»?
Що може собі дозволити «митець» в «творчій майстерні» (ліпнину із завитушками), на «конвейрі» не прийнятне.

Розумію. Працюю на «конвеєрі» в контексті того, що релізимося щороку (продукт). C++17 дозволено. Ніхто не помер.

І справді, чого це я.

тобто коверкання нікнейму, це нормас?

Ну якщо цитувати особисто мій статус з профілю ДОУ і в цьому ж контексті говорити про «манямірок» це нормас, то що тут взагалі може бути не нормас?
Хіба що незавуальовані матюки в адресу одне одного — і то тільки тому, що модератори потруть.

Тобі не подобається твій статус, ну то таке.

Але я його не перекручував.

Я вказав, що манямірок в невеликій фірмі відрізняється від «конвейєрного підходу».

Так що культуральний рівень «Мєнєджєрка Проєктів» під питанням.

Мені він подобається як жарт. Жарт про новий український правопис і моє скептичне до нього ставлення. Але це взагалі поза межами цієї дискусії.

В даному випадку мені не сподобалося, що ти моє професійне життя (особисто моє, судячи з цитування мого статусу) називаєш «манямірком». Саме це слово огидне і походить від бидляцького тюремного жаргону. Але навіть якщо абстрагуватися від його походження, воно означає, начебто той, про кого йде мова (в даному випадку я), живе у світі своїх фантазій, який нічого спільного з реальністю не має. А це теж не так. В реальності проекти є різні. Десь буде справедливо моє ставлення, десь твоє. Кожен собі шукає, що йому ближче. Для обох сторін роботи на ринку вистачає.
Я принаймні собі не дозволяю стверджувати, що хтось, хто працює не так, як мені подобається, живе у «манямірку». Так що про виховання — «чья бы корова мычала».

Про фірму вже сказав. Моя хоч і за розмірами не Єпам чи Глобаллоджік, але теж не маленька. Релізи регулярні, клієнтів вистачає. Між двома крайнощами «веслаємо як на конвеєрі і нічого нового не придумуємо» та «можна експериментувати і вводити свої велосипеди в продакшен в якості нових рішень» у нас дотримується жорсткий баланс, в жодну з них ми не кидаємось. Постійним велосипедінгом, як в невеличких фірмах, ми не страждаємо — але раз на рік можемо і щось таке робити, і інколи воно виявляється справді зручним і допомагає команді.

Див. мою дискусію з Віктором, де я писав, що

«играться» я раз в энное время тоже могу. Здесь вопрос в качестве «игры».

, та й він не заперечував, що таке справді буває:

Когда некий сеньер с 20 годами опыта пишет какой-то модуль с шаблонной магией и всем этим удобно пользоваться.

Ти можеш виправдовуватися скільки завгодно, але це не спростує той факт, що

Мєнєджєрка Проєктів в ТОВ «ИРІЙ»

дозволив коверкати чужий нікнейм

флюман

Тим більше, що ти представляєш тут не тільки себе, а і свою контору.

З.І.
Може вибачишся за

флюман

?

Якщо для тебе мої аргументи це лише спроби виправдатися, то розмова закінчена.
Інакше можеш сам почати з вибачень за

там ТОВ «ИРІЙ», і свій манямірок митців

— після цього, можливо, я розгляну твою пропозицію.

доу пам«ятає
dou.ua/...​orums/topic/12773/#656001

Ну це повний ппц, якийсь с-раний троль («23-лєтній сєньйор») знов нічого по суті сказати не може і намагається неаргументовано траліровать. Йди вчи 1C, ембеддед лайно.

Я тебе теж пам’ятаю. З першого ж коменту почав траліровать, мовляв, «плюси лайно, вчи джаву» (dou.ua/...​orums/topic/12773/#649822).
Так, я тоді задав дурне питання, бо був ще зовсім джуном майже без досвіду. Ще й в той час закінчував універ, де до використання правильної термінології ставилися дуже прискіпливо.
Нікого образити формулюванням не хотів (хоча дехто образився) — просто щиро і наївно поцікавився, як стандартну бібліотеку прийнято називати серед професіоналів.
До проходження співбесід (dou.ua/...​orums/topic/12773/#653441), до речі, це також жодного відношення не мало. Я тоді їх ще й не проходив.
Для людини в подібній ситуації подібні питання не є чимось таким вже й дивним.
А ти написав ось таку фігню. Відповідь тобі закономірна.

На всякий випадок, якщо ти натякаєш на формулювання «ембеддед лайно» начебто це було звернення до тебе (мовляв, «ти — ембеддед лайно»), то ні. Лайном тебе як людину я не називав.
Я написав «ембеддед лайно» в тому ж контексті, в якому ти назвав лайном плюси, тому що ці фрази однаково безпідставні, і у тебе в той час в статусі стояла позиція, пов’язана з ембеддедом.

А от ти мене тоді назвав

якийсь с-раний джун

(dou.ua/...​orums/topic/12773/#655989)

Тому йди в с-раку зі своїми вимаганнями вибачень.
Хоч і лайном я тебе не вважаю, але й поваги до тебе у мене також немає. Тим паче бажання вибачатися з грубість в твою адресу, коли ти себе з самого початку поводиш аніскільки не краще.

Як 5 років тому мав мінімум софт скілів, з таким рівнем і лишився, зато придумав собі фейкову личку

Мєнєджєрка Проєктів в ТОВ «ИРІЙ»

Не тобі говорити про чиїсь софт скіли, любитель кидатися образами і потім вимагати вибачень за відповідне ставлення до себе.

хорошо папочка, прости

не совсем понятно какую именно проблему решает именно такое решение

к.м.к. вопрос решаемой проблемы из «некоторой ложности» исходных аргументов

Для динамічної, часу виконання, диспетчеризації потрібно знати тип об’єкту. C++ має механізми RTTI які можна використати для тих проєктів, що вже використовують RTTI чи можуть собі дозволити таку залежність. Проте рішення не буде чепурним, якщо не надасть альтернативу для проектів, де використання RTTI було б занадто обтяжливим.

Традиційно, подвійна диспетчеризація виконується через віртуальний метод, у якому реалізована ручна диспетчеризація наступного рівня через оператори if чи switch.

вообще-то традиционно н-диспетчеризация в си++ реализуется через цепочку из н вызовов виртуальных функций поскольку каждый вызов виртуальной функции и есть «знание типа объекта» другое дело что виртуальная таблица в си++ только одна и соотв. «из коробки» доступна только 1-уровневая рантайм диспетчеризация

конечно в конце такой цепочки всё сводится к н-степени набору функций класса

A::add(B& b);

что есть непосредственной уже реализацией вариации

add(A& a, B& b);

отмаршрутизированной из абстрактного

add(Base& a, Base& b);

но поскольку мы вводим ещё и шаблоны но каждая такая отдельная реализация конца цепочки виртуальной диспетчеризации читай рантайм полиморфизма сводится к статическому полиморфизму по уже выведенных через динамическую диспетчеризацию классам

template<class A, class B>
add(const A&, const B&) {
a + b;
}

A::add(B& b) {
add(*this, b); // => add(A a, B b);
}
Можна також повернутись до типового рішення подвійної диспетчеризації через віртуальні методи у якому використати наші чепурні шаблони для наступного рівня диспетчеризації.

аб том и вапрос что не вполне ясно зачем вообще эти чепурни шаблоны в конкретной задаче и какую конкретно проблему они решают? потому что введённых по ходу дополнительных «штук» уже вагон и тележка

Зазначимо, що такий підхід потребує підтримки move семантики для типу результату. Для нашого прикладу це не суттєво, проте для інших сценаріїв використання слід враховувати цю особливість.
Розглянемо тепер як можна обійти залежність від RTTI, що виникла через наше використання typeid. Для того щоб підтримати сценарії використання без RTTI, прикладна модель має реалізовувати якийсь механізм користувацької ідентифікації та інтроспекції типів (КІІТ). Наприклад, це може бути призначений вручну чи автогенерований числовий ID для кожного класу. Для автогенерації можна використати геш від __PRETTY_FUNCTION__:
Нажаль, std::hash ще не є constexpr, тож доведеться написати і свою геш функцію (наприклад, таку як в [14]). Тепер ми можемо призначати ID нашим класам:

вместе с тем «традиционная н-диспетчеризация» через цепочку виртуальных функций решает все эти задачи

а) без применения class_id
б) и соотв. без применения доп. логических условий по ходу вызова

и никакие из «вероятно предложенных» задач не оставляет не решёнными

тогда зачем это нужно?

либо же ж ряд «веророятно предложенных задач» остался не озвученными явно

По моему,

A::add(B& b);

вводит зависимости всех классов от всех. И это — треш. А еще — оно вводит зависимости классов от алгоритмов добавления, если эти алгоритмы сложные. Вместо того, чтобы максимально разделить домен, все спутывается в матрицу, где все знают обо всех.

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

Я бы делал через фабрику, которая получает два числа-параметра, внутри через typeid, dynamic_cast, поля или методы находит их типы, и возвращает алгоритм, который умеет с ними работать. Если автор такую фабрику завернул в шаблоны, и все, что нужно — подключить хедер, и заработает глобальный operator+ на любой паре типов — ну супер, это еще лучше, наверное.

Если там для этого сами базовые классы не нужно менять — код такой страшный, что я не распарсил. Если нужно в них лезть — тогда все равно по-старинке за фабрику, энкапсулирующую выбор алгоритма. Так базовые классы ничего не знают друг о друге, и об алгоритмах. И каждый алгоритм знает только те два класса, с которыми работает.

Короче, вот быстрое решение:

shapes.h

#define ADD_SHAPES(a, b) GetAlgoFromTypes(a, b).Add(a, b)

enum ShapeType {
    ST_CIRCLE,
    ST_SQUARE,
    ...
    ST_LAST
};

class Shape;
class ShapeAlgo;
const ShapeAlgo& GetAlgoFromTypes(const Shape& a, const Shape& b);

class Shape {
friend const ShapeAlgo& GetAlgoFromTypes(const Shape& a, const Shape& b);

protected:
    Shape(const ShapeType type) : type_(type) {}

private:
    const ShapeType type_;
}

algo_dispatcher.cpp

#define POWER 4    // power of 2 for algo matrix dimension
#define DIM        (1 << POWER)

STATIC_ASSERT(DIM > ST_LAST);

static const ShapeAlgo* const s_matrix[DIM][DIM] = {
    {&CircleToCircle, &CircleToSquare, ...},
    {&SquareToCircle, &SquareToSquare, ...},
    ...
};

const ShapeAlgo& GetAlgoFromTypes(const Shape& a, const Shape& b) {
    return *s_matrix[a.type_][b.type_];
}

main.cpp

Circle a;
Square b;
result = ADD_SHAPES(a, b);

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

У вашому рішенні я бачу такі особливості:
1. кінцеві функції потребують downcast для параметрів
2. не підтримується коваріантність параметрів
3. не підтримується коваріантність результату
4. заповнюючи таблицю легко помилитись
5. ES.31: Don’t use macros for constants or «functions»

1. кінцеві функції потребують downcast для параметрів

Вирішується макро декларацією функції:

#define DECLARE_SHAPE_METHOD(op, x, y) Shape* op##x##y(const Shape& a, const Shape& b)
#define DEFINE_SHAPE_METHOD(op, x, y) \
    DECLARE_SHAPE_METHOD(op, x, y) { \
    x& left = static_cast<x>(a); \
    y& right = static_cast<y>(b);
2. не підтримується коваріантність параметрів
3. не підтримується коваріантність результату

 Не розумію. Розшифруйте, будь ласка.

4. заповнюючи таблицю легко помилитись

Напишіть автогенератор з енуму на Пітоні) Насправді, не легко — там в кожному рядку та колонці один за аргументів однаковий — одразу буде видно проблему. Та й перший запуск зразу покаже, де впало.

5. ES.31: Don’t use macros for constants or «functions»

Без проблем — можна отак зробить:

namespace shapeop {
    typedef Shape* (*ShapeOperator)(const Shape& a, const Shape& b);
    const size_t power = 4;
    const size_t c_dim = 1 << power;
    inline Offset(const Shape& a, const Shape& b) {return (a.type_ << power) | b.type_;}
    extern const ShapeOperator s_plus_operators[c_dim * c_dim];
    extern const ShapeOperator s_minus_operators[c_dim * c_dim];
};

inline Shape* operator+(const Shape& a, const Shape& b) {
    return shapeop::s_plus_operators[shapeop::Offset(a, b)](a, b);
}
inline Shape* operator-(const Shape& a, const Shape& b) {
    return shapeop::s_minus_operators[shapeop::Offset(a, b)](a, b);
}

А можете у Вашій імплементації навести приклад класу фігури. Що від нього потребується, щоб темплейти запрацювали? Бо я не можу розпарсить. В мене, наприклад, додалося одне поле в базовий клас, а в дочірніх класах — викликається конструктор базового з типом фігури.

1. Тут ви використовуєт для макро для задач що спонукали творців C++ придумати шаблони
2. Covariance and contravariance. У нашому випадку це можливість використати функції що повертають субтип чи задиспетчетись на функцію що приймає супертип, наприклад Rect* intersect(const Rect&,const Rect&)
4. Навіщо доручати пітону ту роботу з якою добре справляється C++?
5. При використанні RTTI достатньо віртуального деструктора, чи будь якого іншого віртуального метода і то це потреба самого RTTI. Без RTTI:

  static constexpr auto classid = class_hash<Rect>();
   bool instance_of(const class_info& expected) const noexcept override {
    return classinfo(*this) == expected or Shape::instance_of(expected);
  }
1. Тут ви використовуєт для макро для задач що спонукали творців C++ придумати шаблони

Навіщо робить складно (і нечитаємо) те, що можна просто зробить макросом?

У нашому випадку це можливість використати функції що повертають субтип чи задиспетчетись на функцію що приймає супертип, наприклад Rect* intersect(const Rect&,const Rect&)

Якщо хтось вже писав цю задачу — все одно потрібен буде перехідник до його імплементації. Якщо не писав — то спокійно можна написать усі функції з однаковими сигнатурами.

4. Навіщо доручати пітону ту роботу з якою добре справляється C++?

Я особисто не розумію, що відбувається у Вашій імплементації. Розшифровки чи прикладів кінцевого коду — також нема. Тому й не знаю, з якою роботою справляється С++, і наскільки там ефективний рантайм.

Без RTTI:
static constexpr auto classid = class_hash();

А далі? Перевіряти усі варіанти по-одному замість того, щоб піти в таблицю за офсетом? Повільніше буде.

спокійно можна написать усі функції з однаковими сигнатурами.

Повторюсь. Створюючи матрицю функції ви обмежуєте їхню сигнатуру до самих базових класів. Тобто функції для конкрентих класів мусять використовувати down cast. Ви запропонували static_cast(a), але це не є type safe, бо ж функції можна викликати і без диспетчера, де класи ідентифікувались.
Безпечніше там використати dynamic_cast (Type.2: Don’t use static_cast to downcast: Use dynamic_cast instead.) Тобто must have RTTI.
Ну і заповнити та підтримувати таку матрицю для 25 класів це той ще челенж.

Тому й не знаю, з якою роботою справляється С++, і наскільки там ефективний рантайм.

У статті є посилання на приклади на godbolt. Там також можна подивитись дизасемблер. Виглядає не набагато страшніше ніж switch

функції можна викликати і без диспетчера, де класи ідентифікувались

 І арифметику на вказівниках можна зробити. І в векторі залізти за межі через operator[](). І ще багато чого можна. Коли хтось викливає AddSquareAndTriangle(), надаючи кола як аргументи — його проблема. В нормальному випадку хедер з конкретними функціями операцій, тіла функцій, та таблиці лежать в окремій папочці, і їх ніхто ззовні не бачить і не інклюдить. А глобальні оператори — бачать і використовують.

Безпечніше там використати dynamic_cast

З котрої це пори в С++ безпека важливіша за швидкість? Може, ще при доступі до елементів вектору варто перевіряти його розмір?

Ну і заповнити та підтримувати таку матрицю для 25 класів це той ще челенж.

* По-перше, усі ці функції треба ще напсати. 25*25 функцій. І таблиця буде незрівняно меншим об’ємом роботи.
* По-друге, в таблиці одну функцію можна використовувати для різних операцій.
* По-третє, геть не загажені самі класи фігур, себто — не порушений single responsibility principle, і нема coupling між різними фігурами, та між фігурами й алгоритмами.
* Вчетверте, таблиця швидше працює)

Себто, вибір між простим і швидким кодом з одного боку, та складним й повільнішим — з іншого. Так, простий код в стилі С, а складний — в стилі С++. Були колись такі суперечки, котра з цих мов швидша.

Так, простий код в стилі С, а складний — в стилі С++.

Я протєстую ваша чєлюсть!
C++ не нав’язує такий стиль, як пропонується автором даної статті. Це лише один з багатьох можливих підходів, які можуть використовуватися у цій мові, але він не єдиний. ТруЪ плюсовик, у якого за плечима вже достатньо досвіду, зможе обрати оптимальний варіант для вирішення конкретної задачі, навіть якщо код виявиться простим і ближче до сішного.

Коли хтось викливає AddSquareAndTriangle(), надаючи кола як аргументи — його проблема.

На проєктах обсягом більше 3-х людиномісяців усе що можна misuse буде misused.

З котрої це пори в С++ безпека важливіша за швидкість? Може, ще при доступі до елементів вектору варто перевіряти його розмір?

type safity важлива. З котрої пори не пригадаю. Мабуть із початку самих плюсів. Загляньте наприклад в JSF AV rules, чи ISO CPP Core Guidelines.

По-перше, усі ці функції треба ще напсати. 25*25 функцій. І таблиця буде незрівняно меншим об’ємом роботи.

Наявність 625 функцій зовсім не обов’язкова. Багато випадків можуть покриватись коваріантими типами параметрів. Тобто діспетчитись по супертипу.
В [2] розлядають випадок із 40 функцій. Я тут викладав свій код для цього кейсу (щось не можу його знайти, мабуть його поховали). Якщо ви кажете що заповнити матрицю просто, ось вам лінк на 25 класів і 40 визначених на них функції. godbolt.org/z/4n1xd8 . Заповніть вашу матрицю і проілюструйте нам простоту цього процесу

По-друге, в таблиці одну функцію можна використовувати для різних операцій.

Можна і треба. Покажіть тільки як.

По-третє, геть не загажені самі класи фігур, себто

Рішення з цієї статті підтримує обидва способи — хоч методами хоч функціями. А матрицю методами не заповниш, бо різнотипні.

Вчетверте, таблиця швидше працює)

Тут заперечувати не буду. У мене вийшло 545 інструкцій на диспетч. Ви мабуть і в 50 вкладетесь. Питання в тому чи аплікація може дозволити ці 500 циклів чи ні.

На проєктах обсягом більше 3-х людиномісяців усе що можна misuse буде misused.

Впаде, побачать, і виправлять. Хто не вміє в С++ — є багато безпечних мов на зразок Расту.

type safity важлива. З котрої пори не пригадаю. Мабуть із початку самих плюсів. Загляньте наприклад в JSF AV rules, чи ISO CPP Core Guidelines.

Не за рахунок швидкості або coupling. Знову ж, кому важливіше — є Раст для таких.

Тобто діспетчитись по супертипу.

З таблицею можна диспетчитись будь-яким методом, не лише за суперкласом. Наприклад, трикутник може не буть субкласом полігону в наслідуванні, але для перетину з колом його має сенс розглядати як полігон. Тому, табличний иетод більш гнучкий.

Якщо ви кажете що заповнити матрицю просто, ось вам лінк на 25 класів і 40 визначених на них функції. godbolt.org/z/4n1xd8 . Заповніть вашу матрицю і проілюструйте нам простоту цього процесу

Поясніть, чому у Вас від 1 до 5 по 4 метода, а далі — по одному?
Ось мій «складний» варіант заповнити таблицю:

input = ("Square", "Triangle", "Circle")
for i in input:
    for j in input:
        print("&Add"+i+"To"+j, end=", \t")
    print()
Можна і треба. Покажіть тільки як.

Руками поставити в потрібні місця. У Вас не руками? І усі випадки симетричні?

Рішення з цієї статті підтримує обидва способи — хоч методами хоч функціями.

Ще раз перепитаю: як в результаті виглядають класи фігур (базовий та похідні) і які дописи в них з’явились для підтримки диспатчу?

Питання в тому чи аплікація може дозволити ці 500 циклів чи ні.

Заради чого? В мене код простіший.

Впаде, побачать, і виправлять.

На деяких проєктах «впаде» звучить буквально і занадто загрозливо.

З таблицею можна диспетчитись будь-яким методом, не лише за суперкласом.

То де ж ваш код?
Мій ось godbolt.org/z/1caGTs
У цьому прикладі чотири namespace:
given — те що дано в godbolt.org/z/4n1xd8
multimethods — шаблони для мультиметодів із цієї статті
solution — рішення завдання
test — тестування рішення.
Все працює. Покажіть тепер ваше матричне рішення для цього прикладу

Наприклад, трикутник може не буть субкласом полігону в наслідуванні, але для перетину з колом його має сенс розглядати як полігон. Тому, табличний иетод більш гнучкий.

Який результат ви очікуєте якщо передасте трикутник у функцію що очікує на вході полігон і при цьому трикутник не є субкласом полігону?

Поясніть, чому у Вас від 1 до 5 по 4 метода, а далі — по одному?

Так дано. Можна задати по іншому. Головне щоб покривались основні кейзи — субклас+субклас, субклас+суперклас, суперклас+субклас, суперклас+суперклас.

Руками поставити в потрібні місця.

То чого ж ви ще не поставили руцями?

У Вас не руками?

У мене копіпастою.

І усі випадки симетричні?

Ні, не всі випадки симетричні.

Ще раз перепитаю: як в результаті виглядають класи фігур (базовий та похідні) і які дописи в них з’явились для підтримки диспатчу?

Ось такі:

  static constexpr auto classid = N;
  virtual bool instance_of(const multimethods::class_info& expected) const; 
Заради чого? В мене код простіший.

У вас код простіший для трьох класів. А для 25 та 40 функцій його не має.

Покажіть тепер ваше матричне рішення для цього прикладу

1) Нема умов прикладу
2) Мені не платять за цей код

Який результат ви очікуєте якщо передасте трикутник у функцію що очікує на вході полігон і при цьому трикутник не є субкласом полігону?

У фігур може буть спільний метод для отримання списку вершин, визначений вище полігона. Класичні приколи en.wikipedia.org/...​ki/Circle—ellipse_problem

То чого ж ви ще не поставили руцями?

Бо ви не заплатили.

У вас код простіший для трьох класів. А для 25 та 40 функцій його не має.

Допишіть в верхньому рядку назви ще 37 фігур, і автоматом буде створена матриця для 40.

input = («Square», «Triangle», «Circle»)
for i in input:
for j in input:
print("&Add"+i+"To"+j, end=", \t")
print()
1) Нема умов прикладу

Ще раз умови: 25 класів і 40 методів визначених тут godbolt.org/z/4n1xd8

2) Мені не платять за цей код

Якщо це так просто то ви б витратили на це менше часу ніж на диспути. Я свій приклад зробив за 20 хв.

Бо ви не заплатили.

Не можете написати, то так і скажіть, навіщо оці дитячі відмазки?

Допишіть в верхньому рядку назви ще 37 фігур, і автоматом буде створена матриця для 40.

Класів 25, а методів не 625 а лише 40. Різницю бачите?

Класів 25, а методів не 625 а лише 40. Різницю бачите?

А що в тілі методів?

Що небудь аби не пусто. В [2] використали просте додавання, у мене в прикладі теж додавання, але воно мабуть зоптимізувалось в простий return

Тоді можна згенерувати тим самим скриптом.
В реалі основна робота як раз буде в тілах методів.

З котрої це пори в С++ безпека важливіша за швидкість?

філософське питання...але на нього є відповідь: Руст

Себто, вибір між простим і швидким кодом з одного боку, та складним й повільнішим — з іншого. Так, простий код в стилі С, а складний — в стилі С++. Були колись такі суперечки, котра з цих мов швидша.

Руст:
швидкий, не складний, безпечний, безплатна абстракція: бери все одночасно.

А еще вместо макроса можно поставить

inline Shape operator+(const Shape& a, const Shape& b) {
    return GetPlusForShapes(a, b)(a, b);
}
inline Shape operator-(const Shape& a, const Shape& b) {
    return GetMinusForShapes(a, b)(a, b);
}
И разнести в фабрике операторы в разные таблицы:
typedef Shape (*ShapeOperator)(const Shape& a, const Shape& b);
static const ShapeOperator s_plus_operators[DIM][DIM] = {...};
static const ShapeOperator s_minus_operators[DIM][DIM] = {...};
Тогда оно вообще будет диспатчить за одно обращение в статическую память + 2 обращения к объектам + сдвиг единицы + битовый ор.

ЗЫ: как ты делаешь вставку кода в коммент? у меня таг code почему-то не работает

ЗЫ: тю а теперь работает )) какая-то магия

Тут надо использовать prе, а не code. Или уже пофиксили?

оно работает как-то странно у меня сложилось впечатление что оно не сразу «подсвечивает» и как только закоммитил то сперва не видно что оно на самом деле уже не просто текст а потом оппа и уже подсвечивает как-то так ))

аб том и вапрос что не вполне ясно зачем вообще эти чепурни шаблоны в конкретной задаче и какую конкретно проблему они решают? потому что введённых по ходу дополнительных «штук» уже вагон и тележка

Для рішеннь, які зустрічаються в джерелах, справджується принаймні одне з: складне у використанні, застаріле, обтяжливе, інтрузивне та неструктуроване. Опис вашого рішення я намагався співставити із тим що зустрічав у джерелах, проте не певен що мені це вдалось. Може проілюстуєте більш розгорнуто?

тогда зачем это нужно?

P. Pirkelbauer, Y. Solodkyy, B. Stroustrup вважають так

This is a well-known problem for operations where the choice of a method depends on the types of two or more arguments («multiple dispatch»),

Мого досвіду недостатньо щоб спростовувати такі узагальнення чи пропонувати інші такого ж рівня, тож я покладаюсь тут на їхній досвід.

struct Shape;
struct Rect;
struct Circle;
struct Square;

struct Shape {
	virtual ~Shape() {}
	virtual Shape* add_shape(const Shape * b) const = 0;

	virtual Shape* add_shape_disp(const Rect * a) const = 0;
	virtual Shape* add_shape_disp(const Circle * a) const = 0;
	virtual Shape* add_shape_disp(const Square * a) const = 0;
};

struct Rect : Shape
{
	Shape* add_shape_disp(const Rect * a) const
	{
		std::cout << "Rect + Rect" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Circle * a) const
	{
		std::cout << "Circle + Rect" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Square * a) const
	{
		std::cout << "Square + Rect" << std::endl;
		return nullptr;
	}

	Shape* add_shape(const Shape * b) const
	{
		return b->add_shape_disp(this);
	}
};
struct Circle : Shape
{
	Shape* add_shape_disp(const Rect * a) const
	{
		std::cout << "Rect + Circle" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Circle * a) const
	{
		std::cout << "Circle + Circle" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Square * a) const
	{
		std::cout << "Square + Circle" << std::endl;
		return nullptr;
	}

	Shape* add_shape(const Shape * b) const
	{
		return b->add_shape_disp(this);
	}
};

struct Square : Rect
{
	Shape* add_shape_disp(const Rect * a) const
	{
		std::cout << "Rect + Circle" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Circle * a) const
	{
		std::cout << "Circle + Square" << std::endl;
		return nullptr;
	}

	Shape* add_shape_disp(const Square * a) const
	{
		std::cout << "Square + Square" << std::endl;
		return nullptr;
	}

	Shape* add_shape(const Shape * b) const
	{
		return b->add_shape_disp(this);
	}
};

namespace calculus {
	struct Expression {
		virtual ~Expression() {}
	};

	struct Constant : Expression {};
	struct Integer : Constant {};
	struct Float : Constant {};
}

Shape* add(const Shape * a, const Shape * b)
{
	return a->add_shape(b);
}

Shape* sub(const Shape * a, const Shape * b)
{
	return nullptr;
}

void do_double_dispatch()
{
	Rect r1;
	Circle c1;
	Square s1;

	add(&r1, &c1); // << "Rect + Circle"
	add(&c1, &s1); // << "Circle + Square"
	add(&s1, &r1); // << "Square + Rect"
}

ЗЫ: по сути это то же ж самое формирование таблицы м на н об которой писал Denys Poltorak

в данном случае 3×3 по 3 виртуальных функции в каждом из 3-х классов

сперва вызывается уровень № 1 маршрутизации который определяет тип параметра № 1 потом уже зная тип параметра № 1 как this делается довызов функции по сигнатуре типа № 1 но с маршрутизацией на тип № 2

фактически функции add_shape_disp можно было записать как add_shape_square впрочем они так и записаны потому что си++ добавляет список типов прямо в название реальной сишной функции в которую это всё г. простите магия )) компилируется

Дякую, що докладаєте стільки зусиль аргументовано опонуючи.
Давайте спробуємо маштабувати ваше рішення на такий приклад:
Є 20 конкрентих класів зрупованих по 4-х суперкласах породжених від одного абстракного — усього 25 класів.
На них визначено 5 операцій: add(X,Y), sub(X,Y), mul(X,Y), div(X,Y), polynomial(X,Y,N)
Для кожної із цих операцій визначено по 40 конретних реалізацій — усього 200 функцій.

Для імплементації диспетчиризації вашим способом до абстракного класу доведеться додати 4*25 (add_*, sub_*, mul_*, div_*) методів, та, важко навіть уявити скільки, polynomial_*. Обмежимо N суперкласами та класами одного суперкласу — усього 9 класів для N, та 25 для Y: 25*(9+1) = 250.
Виходить разом 350 методів. На мою думку, таке роздування абстракного класу занадто інтрузивне. Та все ж продовжимо. Тепер, щоб наш проєкт компілювався, слід реалізувати їх у, що найменше, 4 суперкласах — тобто треба написати 4*350 — 1400 методів. Для диспетчиризації на 200 функції. Як на мене це занадто складно. Але хай буде, рухаємось далі. Написали всі потрібні методи, все працює.
Аж тут виникла потреба додати ще один клас і по дві функції на кожну операцію. Для цього доведеться додати з десяток методів до абстракного класу, і заоверрайдити їх у щонайменше 4 класах, ніяк не пов’язаних із новеньким — 40 методів деінде. То такі витрати на підтримку , на мою думку занадто обтяжливі.

Порівняємо із мультиметодами:
Для імплементації диспетчиризації слід буде додати 25 методів instance_of, та написати 5 фукції що диспетчеризуватимуть із використанням шаблону multimethod
Функції можна додавати по одній, їх додавання не буде розвалювати білд проєкту. Додаючи новий клас немає потреби вносити якісь зміни у існуючі.

Самое понятное и простое для всех сделать в стиле Полторака с матрицей функций (указателей на функции). Самое красивое, модное и сложное для большинства программистов сделать, как ты предлагаешь.

Чомусь такий очевидний спосіб ніхто не пронував у джерелах, що мені трапились. Із контейнерами функцій, пам’ятаю, був тільки заснований на std::map.

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

можна я не буду на это отвечать )) спасибо

ЗЫ: но для ерунды на 25 классов с кучей ерунды я просто напишу генератор и мне будет уже всё равно 25 там классов или 200500 и добавление нового сведётся к рутине «делай раз делай два» равно как и все «правки по коду»

ЗЫ: впрочем если подумать и отдельную операцию просто проксировать то тоже должно работать но я подумаю как-нибудь потом

как эксперт по всех этих проблемах с какими уже приходилось сталкиваться и вероятно и придётся в любом обозримом будущем главная проблема таких проблем в «обратной постановке задачи» когда берётся какое-то решение и уже под него начинает придумываться и подгоняться как постановка задачи так и вообще уже сама задача такой вот селяви который вы сами не смогли решить на уровне «доказуемых примеров»

У прикладах, викладених на godbolt.org компілятор gcc генерує оптимізований код із статичною диспетчеризацією. Щоб була задіяна, динамічна диспетчеризація слід рознести test функції прикладів по різних одиницях компіляції.
ЗЫ: но для ерунды на 25 классов с кучей ерунды

Приклад із 20 класами — з [2].

я просто напишу генератор и мне будет уже всё равно 25 там классов или 200500 и добавление нового сведётся к рутине «делай раз делай два» равно как и все «правки по коду»

Як напишете, приносьте подивимось.

вот селяви который вы сами не смогли решить на уровне «доказуемых примеров»

Що до цитати — то вона про gcc а приклади компілюються із clang.

А давайте статтю напишемо?
Покажемо, що мій метод швидший та простіший за наведені в літературі...
В Overload magazine, наприклад.
Ви вже зробили літературний огляд. Тепер англійською перекласти — і погнали. Можна буде до таблиці звести учі відомі рішення, їх ціну та швидкість.

А давайте статтю напишемо?
Покажемо, що мій метод швидший та простіший за наведені в літературі...

То покажіть же ж нарешті.

Можна буде до таблиці звести учі відомі рішення, їх ціну та швидкість.

Таблиця із зведеними порівняннями є в [2]. Чомусь там такий простий метод як ваш обійшли увагою.

То покажіть же ж нарешті.

* Звернення до об’єкта першого параметра, вичитати тип = читання за вказівником з офсетом
* Звернення до об’єкта другого параметра, вичитати тип = читання за вказівником з офсетом
* Вирахувати офсет в таблиці = бітовий зсув та бітовий ор
* Звернення до таблиці = перехід за вказівником з офсетом
Маємо кілька простих арифметичних та бітових операцій і 3 доступи до пам’яті.

То не код а текст. Він не компілюється, і не вирішує задачу 25 класів 40 методів

Ну ок. Мені ліньки писати, Ви не можете зрозуміти простий C-style код, і публікації не буде.

Та то не ліньки. Ви просто заявили більше ніж можете подужати і тепер задкуєте.

Так, я не хочу витрачати пів-години чи годину на пітонівський скрипт.

Я без пітону, просто на плюсах, за 20 хв все зробив. А генерувати код хоч на пітоні хоч чим іншим — як тільки доходиш до коваріантності усе зразу стає страшенно складно.
Просто проблема множинної диспетчиризації значно глибша ніж видається з першого погляду

Може вона й глибша, та має просте й швидке рішення, що не засмічує класи, і не вносить в них зайвих залежностей. Інші рішення — повільніші, а коли можна повільніше — то використовують не С++, а джаву чи пітон.

Цікаво чи можна зробити кращий dispatch що не потребує common return value type (відкинемо зараз ситуацію з віртуальними класами, нехай всі типи відомі). Зараз я бачу два мінуси — ReturnType має бути загальним для всіх функцій і, а також default constructible.

Але ж всі типи відомі — як функцій так і аргументів, чому не можна вивести ReturnType зробивши match на етапі компіляції а також не створювати його на стеку.

Я дивився схожий приклад в boost::mp11 де вони робили свій std::visit: www.boost.org/...​s/mp11/doc/html/mp11.html
То там пацани теж використовують common type у вигляді std::variant.

ReturnType не має бути одинаковим, але коваріантним. Наприклад якщо задати Shape* то методи можуть повертати також Circle*, Rect*, Square*. Про default constructible — тут з вами згоден. Треба буде додати у текст.
Я думав про виведення ReturnType проте простого рішення не знайшов. А залучення std::variant видалось мені дещо обтяжливим і інтрузивним. Що до створення ReturnType на стекові — то це для згортки параметрів. Якщо знаєте як згорнути по іншому — поділіться.

А знаєте, що роблять в кривавому продакшені, коли от таке насправді стає потрібно? Правлять вручну vtable. Звучить дико, але це робить заради швидкодії, наприклад, DirectX. А з 100500 рівнями вкладених шаблонів ніхто розбиратися не буде.

Не уявляю як можна штучно роздути vtable до 400 методів, як у прикладі з open methods [2]

Навряд чи хтось захоче таке використовувати в продакшені. Зазвичай у продуктових командах не так багато людей настільки добре володіє темплейтної магією, щоб їм було комфортно працювати з подібними вундервафлями. Простіше захардкодити типи з ієрархії у окремих віртуальнтих функціях, реалізувавши класичний double dispatch, і не паритися. Тим більше що при нормально продуманій архітектурі множина цих типів міняється досить рідко (якщо взагалі міняється).
Але в рамках самоосвіти покопатися в такому, звісно, зайвим не буває.

Передизайнивши архітектуру можна взагалі обійтись без подвійної диспетчеризації. Проте стаття не про це, а про те як не боятись темплейтної магії :)

Не ожидал услышать голос разума в этой теме :D

Да, знаю этот распространённый среди ДОУ-эмбеддеров стереотип, якобы плюсовики все поголовно любят оверинжинирить и лепить усложнённый код с темплейтами везде просто потому что это круто, мол, «посмотрите какой я вумный».
Он не соответствует действительности. Лично я перерос такой этап где-то на стадии между джуном и миддлом. Хотя темплейтные извращения люблю до сих пор и достаточно хорошо в них разбираюсь — но без реальной необходимости не применяю. Как в той шутке про орфографию и мастеров кунг фу :)
А большинство моих коллег-плюсовиков вообще такими делами не интересуется. И правильно делают, ибо в продакшене оно нужно раз полгода от силы.

Он не соответствует действительности.

К сожалению, соответствует.

Лично я перерос такой этап где-то на стадии между джуном и миддлом.

Это замечательно.

Хотя темплейтные извращения люблю до сих пор и достаточно хорошо в них разбираюсь — но без реальной необходимости не применяю.

Чем больше знаешь, тем больше понимаешь, к месту оно или нет.

К сожалению, соответствует.

К счастью, не соответствует.
Я не такой, все мои коллеги что с прошлой (геймдев), что с нынешней (десктоп) работы не такие. Значит уже не все поголовно.
Это тенденция джунов, которые уже приобрели знания о языке и всяких его извращённых техниках, но ещё не поняли, как это соотносится с настоящими «боевыми» проектами и насколько оно там уместно.

Чем больше знаешь, тем больше понимаешь, к месту оно или нет.

Тут даже «чем больше знаешь» не в контексте хард-скиллов вроде каких-то специфических знаний C++ и темплейтной магии.
А чем больше у тебя опыта в реальной работе на продакшене. Чем больше ты видел разработку с разных сторон (не только с позиции кодера), чем больше понимаешь её приоритеты и т.д.
Чисто вопрос опыта.

Поэтому я и скептически отношусь к твоим, Фогола, Нурибекова и flyman’а (вроде никого не забыл?) комментам про таких вот плюсовиков.
Такое впечатление, что у вас так составлены вакансии, что только джуны на них и отзываются, а остальные просто не идут даже попробовать пособеседоваться.

Тут проблема с тем, что когда ты идешь на С++ собеседоваться, то очень часто начинают спрашивать что-то на знание стандарта и новых фич. Благо есть где разогнаться. В результате на проекте собираются одни энтузиасты метапрограммирования и С++20.

что-то на знание стандарта и новых фич

Меня тоже такое спрашивали про некоторые положения стандарта (например, что он гарантирует и чего не гарантирует про размеры базовых типов), про новые фичи (например, чем отличается make_shared от new + shared_ptr ctor). Про метапрограммирование не спрашивали. Всё было адекватно.
По-моему, тут проблема в отдельных собеседователях, у которых цель не человека в команду подобрать, чтобы проект пилить и деньги зарабатывать совместными усилиями, а тупо блестнуть умом, посоревноваться и самоутвердиться. Я про таких слышал. Лично, к счастью, не сталкивался, и слава богу. Для меня это был бы звоночек, что что-то с конторой не так.

Ну вот мне вообще не интересны положения стандарта и фичи. Может — потому, что в жизни никогда не были нужны. Я их и не знаю.

Не прибедняйся :)
Явно некоторые фичи тебе были нужны. Могу даже вспомнить один пример лично от тебя, когда тебе пригодились variadic templates + perfect forwarding, чтобы избавиться от дублирования темплейтных методов в самописном контейнере.

Хотя в целом да, я понимаю о чём ты: опять специфика эмбеддеда. Где ты должен знать конкретно те платформы, под которые разрабатываешь, во всех деталях, а на остальные так-то пофиг, — здесь знать требования стандарта и правда не нужно. Можно хоть UB писать, если ты точно знаешь, что на конкретной платформе с конкретным компилятором оно будет вполне себе defined.
Да и фичи современного C++ в эмбеддеде тоже мало где нужны, поскольку там значительная часть кода низкоуровневая, более близкая к «Си с классами», чем к идиоматичным плюсам. (Правда, к constexpr и [[nodiscard]] я бы присмотрелся всё равно.)
В таком случае я просто не понимаю, зачем на собеседованиях на такие эмбеддед проекты спрашивать то, что в работе не используется.

Но в других областях вопросы про стандарт (если без фанатизма) и фичи (если те реально используются в боевом коде) часто имеют смысл.

Дамы и господа, вы прослушали монолог «представление несостоявшегося геймдевелопера и чуть более состоявшегося десктопника об эмбеддеде».

Но в других областях вопросы про стандарт (если без фанатизма) и фичи (если те реально используются в боевом коде) часто имеют смысл.

Я понимаю и принимаю полезность C++ 98.

Но последовавшие затем стандарты/навороты... Зачем?
Десктоп разработка — это уже "шарпы«/WPFы. Накрайняк QT (но тоже в рамках C++ 98), если требуется портабельность.

Весь эмбед — это C.

Где ниша C++ 03 и последующих стандартов?

Лично я «плюсов» на десктопных проектах не видел, с начала 2010-х — а я работаю близко к девайсам...

Но последовавшие затем стандарты/навороты... Зачем?

Потому что некоторые вещи становятся удобнее и/или безопаснее (override, member initializers, nodiscard, =default/=delete, concepts), некоторые просто появляются такие, что на C++98/03 не были возможны на уровне языка (unique_ptr и прочие move-only типы, constexpr), ещё некоторые стандартизируются из тех, которые раньше были доступны только с использованием нестандартных расширений (thread/mutex/atomic, alignas/alignof, unordered_set/_map, static_assert).

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

Так-то причины для появления новых стандартов есть, и многим вся эта движуха нравится. Включая меня, работающего над десктопным проектом на C++17 и Qt.
По большей части это вопрос личных предпочтений того или иного девелопера, холиварить здесь не о чем, а проектов на рынке ещё долго будет хватать на любой вкус.

Лично я «плюсов» на десктопных проектах не видел

Они как суслик. Ты не видел — а они есть :)

Меня в этом волнует перегруз основного неймспейса. в Питоне, допустим, и в старых плюсах, там было пару десятков более-менее очевидных значений кейвордов и спецсимволов. А в новых — наверное, уже за сотню (учитывая семантически разные варианты, как static или &). И это все структурированно в голову не влазит.

Ну, кому не надо, тому и нет потребности это себе в голову впихивать. Оно по крайней мере не навредит, ибо вряд ли у тебя будет желание назвать переменную именно constinit или co_yield :)
Если даже где-то в программе по стечению обстоятельств окажется такой идентификатор, переименовать его в большинстве случаев не будет проблемой.

Да и насчёт «за сотню» — преувеличение.
Вот: en.cppreference.com/w/cpp/keyword
Не так уж много их добавили начиная с C++11, большая часть уже существовала в 98.

Я скорее не о кейвордах, а о семантике. Когда & кроме адреса может значить ссылку, && оказывается для мув конструкторов, для передачи в темплейт без изменения типа, и кто знает, еще для чего переиспользовано в новых стандартах. В результате — не зная всех новых фич новый код просто нечитаемый, потому что там перл какой-то, а не human-readable кейворды. И предположить, что бы это значило, и как оно должно бы работать, неоткуда — спецсимволы хинтов не дают.

ЗЫ когда я учил Питон и С++, они были почти одного порядка сложности и читаемости. Ну в С++ невменяемые ошибки компилятора и memset(this, 0, sizeof(*this)) почему-то потом крешился. Но по большей части — человеческие слова, и очевидное из кода поведение.

и кто знает, еще для чего переиспользовано в новых стандартах

Ни для чего нового, суть && с 11-ых плюсов не менялась.
&& обозначает rvalue-ссылку, но в одном специфическом контексте может обозначать «forwarding ссылку», которая будет либо lvalue либо rvalue в зависимости от того, что передавалось на входе.
Есть ещё альтернативное объяснение (вместо введения понятия «forwarding ссылок») через reference collapsing rules.
Но щас не суть.

Претензия мне вполне понятна. В защиту конкретно новых стандартов скажу только то, что в плюсах такая ambiguity была всегда.
* — указатель, * — разыменование, * — оператор умножения... Примеры с & и static ты уже приводил. А я ещё добью словом inline, которое даже не все люди с лычкой senior здесь на ДОУ понимают правильно.
Да, сложно. Наверняка могли бы сделать проще, но в своё время не додумались, и так теперь с этим приходится жить из-за обратной совместимости. Щито поделать.
Новые стандарты добавляют контент, который хоть и добавляет удобства и новые возможности, но тащит за собой эту «обратную сторону медали» дальше.
Кому это прям сильно не нравится, остаются на старых стандартах (благо проектов на них тоже на рынке хватает) или переходят на чистую сишку или раст. Остальные по мере необходимости разбираются с нововведениями. Не так уж они и страшны, если уделить их изучению некоторое время. На том же cppreference есть очень даже хорошие объяснения по большинству тем.

А что там с

inline

прям такого сложного ?

То, что это слово не о фактическом инлайнинге (встраивании тела функции в место вызова). А если и о нём, то разве что в последнюю очередь. Хоть и исторически пошло оттуда.
Многие inline функции по факту заинлайнены не будут. Слово разве что может поднять приоритет инлайнинга для компилятора и немного повлиять на его решения.

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

Кстати, кто это понимает, спокойно воспринимает inline переменные из C++17. Для таких людей это кажется вполне логичным. В то время как другие, думающие что inline для инлайнинга, офигевают и у них начинает взрываться мозг. Мол, «как это так, переменную инлайнят, что тут вообще происходит?» :)

Спасибо, я даже не знал, что в с++17 инлайн для глобальніх переменных добавили.

Ничего, кроме того, что оно уже давно значит не то, какой в него смысл заложили люди.
Более того в разных контекстах оно значит разное. Всё то же и со словом static. А еще register и т.п.

register

тютю уже регистер. задепрекейтили

Задепрекейтили уже давно. А начиная с C++17 вообще удалили из языка. Теперь «The keyword is unused and reserved».

Один из немногих случаев, когда они не стесняются ломать обратную совместимость между версиями языка. Туда же всякие std::unary_/binary_function и прочие auto_ptr.

Да я по разным собесам хожу — хай лоад тоже интересен. И платформы толком не знаю — даже ассемблер так и не выучил, и паять не пришлось.

так як згадали вище мене,
скажімо такі спостереження по співбесідам С++

1. Пишуть «вимоги знання С++14, С++17, С++20». Якось я на таке не підписувався, але попалась доволі мудра HR і сказала, давай спробуєм, а там побачим. І виявляється, що максимум це С++11, а решта хіпстерськиї старндарів: «може коли перейдем, як почне підтримувати компілятор». Так що перестав комплексувати щодо «досвіду із сучасними стандартами С++»

2. Часто співбесіда іде в руслі «шукаємо С++ жонглера» і отут у наc UB, на що моя відповідь в дусі анекдота
" - доктор, якщо я так роблю, то в мене болить тут"
" - а не робіть так, і не буде боліти"

3. На інтрев«ю просять обійти дерево. На що відповідь,
Вам тре спеціаліста по «компутер саєнсу», чи розробника програмного забезпечення. Якщо я захочу обходити дерева, то буду подаватися в FAANG.

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

Так що я би не сказав, що при тому стані «кадрового голоду С++» іде адекватний процес інтерв«ю. Точніше «голод», як на мене через те, що С++ настільки розпух, що щоб їм жонглювати, як писали вище, тре 10 років С++ жонглювання.

А співбесіди, проводять «С++ жонглери», які міряють інших по собі (натягують сову на глобус).

В більшості раціональних людей виникненне бажання використовувати інструмент попростіше (Rust/Go, напр.), а не страдати 10 років, щоб ще 20 років мучитися розгрібаючи легасі код.

Так що я би не сказав, що при тому стані «кадрового голоду С++» іде адекватний процес інтерв«ю. Точніше «голод», як на мене через те, що С++ настільки розпух, що щоб їм жонглювати, як писали вище, тре 10 років С++ жонглювання.

А співбесіди, проводять «С++ жонглери», які міряють інших по собі (натягують сову на глобус).

основной вопрос «вот сидит передо мной чувак и мне надо понять могу ли я его использовать для исполнения моих задач» причём если смотреть аккуратно то здесь присутсвуют аж сразу 2 стороны активно участвующие

№ 1 «его использовать»
№ 2 «могу ли я»

и фактической точкой схождения в этом месте будет код как суть уже материализованная абстракция пространства решений и вот самым простым было бы б «покажи код» к чему в частности мы свелись и в этом треде и вот тут и начинается самое интересное ))

В більшості раціональних людей виникненне бажання використовувати інструмент попростіше (Rust/Go, напр.), а не страдати 10 років, щоб ще 20 років мучитися розгрібаючи легасі код.

потому что таки да вот мне надо см. п.п. № 2 таки «разгребать легаси» при этом никак не занося в него

інструмент попростіше (Rust/Go, напр.)

ну хотя бы б потому что человек пришёл ко мне на си++ ну положим он знает слова «руст» и «гоу» ну ок молодец возьми с полки пирожок а чем человек может быть полезный для меня что он для меня может сделать полезного?

ЗЫ: так например ну скромно так половина чуваков не могут кодить вот скажем сесть и «в уме» набросать что-нибудь вроде контейнера чисто абстрагируясь от хранения как рабочий вариант завернув его в стандартный контейнер но вот просто чисто внешний интерфейс со всей положенной по задаче инфраструктурой одного аж целого класса включая поле класса в котором непосредственно будет храниться тот самый стандартный контейнер

и здесь с одной стороны вроде как и не совсем правильно ожидать «писать код» от человека на интервью а с другой стороны а чего лично мне см. п.п. № 2 ожидать от человека на интервью если см. п.п. № 1 потому что если не смотреть а потом смотреть уже код «в продакшине» а там вот такие сабжевые 4-этажные темплейты которые даже толком не может объяснить как работают (здесь обобщение не как конкретно сабж но скорее сабж как иллюстрация)

Пишуть «вимоги знання С++14, С++17, С++20».

таки да это буллшит )) с другой стороны многие «кандидаты» и заявляют что хотели бы б нового в т.ч. дословно

В більшості раціональних людей виникненне бажання використовувати інструмент попростіше (Rust/Go, напр.), а не страдати 10 років, щоб ще 20 років мучитися розгрібаючи легасі код.

но вот лично у меня такого «нового» ничего нет лично мне им в этом плане предложить нечего так что же ж мне делать?

и да лично у меня в этом месте возникает тот самый «кадровый голод на людей которые могли бы б что-то полезного сделать для меня» такой вот селяви ничего личного просто бизнес

ЗЫ: ну это не страшно поскольку лично я сам нахожусь ровно в той же ж позиции по другую сторону когда уже я выступаю как «ну ок вот он я и что же ж именно полезного лично я могу сделать конкретно для _вас_?» )) тоже ничего личного тоже просто бизнес

т.е. просят обойти дерево только на интервью на плюсы?

Якщо підбити статистику, то останній раз на плюсах на інтрев«ю просили вичислити глибин.

Колись ще на русті теж,
і ще рік чи два тому було один раз на «домашнє завдання», по якому сказали, що хочуть щоб пореробив, але без рекурсії,
і ще років 3 тому, теж «домашка», але там навіть без фідбека.

А що?

А що?
скажімо такі спостереження по співбесідам С++

3. На інтрев«ю просять обійти дерево. На що відповідь,
Вам тре спеціаліста по «компутер саєнсу», чи розробника програмного забезпечення. Якщо я захочу обходити дерева, то буду подаватися в FAANG.

так таки так, якщо на інтерв"ю почну обходити дерева, то моє ЧСВ підніметься до розуміння, що пора влаштовуватися в FAANG а не на місцеві галери

Причем тут вакансии? Мне приходится работать с тоннами C++ кода (почти все драйвера написаны на плюсах) и выборка у меня гораздо больше чем коллеги с прошлой или текущей работы.

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

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

Я тоже драйвера не писал, но мне кажется там перфоманс на первом месте, по сравнению с читаемостью кода.

Нет, читаемость везде во главе угла. На втором месте перформанс. Без читаемости в саппорте повесятся баги фиксить или фичи добавлять.

было бы неплохо написать, какой с++ поддерживается данной реализацией

Судя по «template<auto Entry, auto ... Entries>» — C++20.

Упс. Случился ДДОС мозга. «template<auto>» — это C++17. И по ссылке на godbolt стоит -std=c++17, так что таки не 20.

не, я то по «фичам» вычислил, но кагбе хотелось бы, что автор сам указывал

Маєте рацію. Я не врахував що «сучасний» теж категорія суб’єктивна і скороминуча. Як тут вже зазначили — це C++17

Гарний реферат, але тут не оцінять

(див. [3], [4], [5], [6], [7], [8], [9], [10])

Подписаться на комментарии