Protobuffers — это неправильно

http://reasonablypolymorphic.com/blog/protos-are-wrong/
  • Перевод
Значительную часть своей профессиональной жизни я выступаю против использования Protocol Buffers. Они явно написаны любителями, невероятно узкоспециализированы, страдают от множества подводных камней, сложно компилируются и решают проблему, которой на самом деле нет ни у кого, кроме Google. Если бы эти проблемы протобуферов остались в карантине абстракций сериализации, то мои претензии на этом и закончились бы. Но, к сожалению, плохой дизайн Protobuffers настолько навязчив, что эти проблемы могут просочиться и в ваш код.

Узкая специализация и разработка любителями

Остановитесь. Закройте свой почтовый клиент, где уже написали мне полписьма о том, что «в Google работают лучшие в мире инженеры», что «их разработки по определению не могут быть созданы любителями». Не хочу этого слышать.

Давай просто не будем обсуждать эту тему. Полное раскрытие: мне доводилось работать в Google. Это было первое (но, к сожалению, не последнее) место, где я когда-либо использовал Protobuffers. Все проблемы, о которых я хочу поговорить, существуют в кодовой базе Google; это не просто «неправильное использование протобуферов» и тому подобная ерунда.

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

Узкая специализация и разработка любителями идут рука об руку. Многое в спецификациях словно прикручено в последний момент — и оно явно было прикручено в последний момент. Некоторые ограничения заставят вас остановиться, почесать голову и спросить: «Какого чёрта?» Но это всего лишь симптомы более глубокой проблемы:

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

Отсутствие композиционности


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

  • Поля oneof не могут быть repeated.
  • В полях map<k,v> есть специальный синтаксис для ключей и значений, но он не используется ни в каких других типах.
  • Хотя поля map можно параметризовать, больше никакой определённый пользователем тип нельзя. Это значит, что вы застряли с указанием вручную собственных специализаций общих структур данных.
  • Поля map не могут быть repeated.
  • Ключами map могут быть string, но не bytes. Также запрещены enum, хотя последние рассматриваются как эквивалент целым числам во всех остальных частях спецификации Protobuffers.
  • Значения map не могут быть другими map.

Этот безумный список ограничений — результат беспринципного выбора дизайна и прикручивания функций в последний момент. Например, поля oneof не могут быть repeated, потому что вместо побочного типа генератор кода выдаст взаимоисключающие необязательные поля. Такое преобразование справедливо только для сингулярного поля (и, как мы увидим позже, не работает даже для него).

Ограничение полей map, которые не могут быть repeated, примерно из той же оперы, но показывает другое ограничение системы типов. За кулисами map<k,v> преобразуется в нечто похожее на repeated Pair<k,v>. И поскольку repeated — это волшебное ключевое слово языка, а не нормальный тип, то он не сочетается сам с собой.

Ваши догадки о проблеме с enum так же верны, как и мои.

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

Решение заключается в следующем:

  • Сделайте все поля в сообщении required. Это делает каждое сообщение типом продукта (product type).
  • Повысить значение поля oneof до автономных типов данных. Это будет тип сопродукта (coproduct type).
  • Дать возможность параметризации типов продуктов и сопродуктов другими типами.

Вот и всё! Эти три изменения — всё, что вам нужно для определения любых возможных данных. С этой простой системой можно переделать все остальные спецификации Protobuffers.

Например, можно переделать поля optional:

product Unit {
  // no fields
}

coproduct Optional<t> {
  t    value = 0;
  Unit unset = 1;
}

Создание полей repeated тоже просто:

coproduct List<t> {
  Unit empty = 0;
  Pair<t, List<t>> cons = 1;
}

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

Сомнительный выбор


Protobuffers в духе Java различает скалярные типы и типы сообщений. Скаляры более или менее соответствуют машинным примитивам — таким вещам, как int32, bool и string. С другой стороны, типы сообщений — это всё остальное. Все библиотечные и пользовательские типы являются сообщениями.

Конечно же, в двух разновидностях типов совершенно разная семантика.

Поля со скалярными типами присутствуют всегда. Даже если вы их не установили. Я уже говорил, что (по крайней мере в proto31) все протобуферы инициализируются нулями, даже если в них нет абсолютно никаких данных? Скалярные поля получают «липовые» значения: например, uint32 инициализируется в 0, а string инициализируется как "".

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

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

Сравните это поведение с типами сообщений. В то время как скалярные поля являются «тупыми», поведение полей сообщений совершенно безумно. Внутренне, поля сообщений либо есть, либо их нет, но поведение сумасшедшее. Небольшой псевдокод для их аксессора стоит тысячи слов. Представьте такое в Java или где-то ещё:

private Foo m_foo;

public Foo foo {
  // only if `foo` is used as an expression
  get {
    if (m_foo != null)
      return m_foo;
    else
      return new Foo();
  }

  // instead if `foo` is used as an lvalue
  mutable get {
    if (m_foo = null)
      m_foo = new Foo();
    return m_foo;
  }
}

По идее, если поле foo не установлено, вы увидите инициализированную по умолчанию копию, просите вы об этом или нет, но не сможете изменять контейнер. Но если вы измените foo, он также изменит своего родителя! Всё это просто чтобы избежать использования типа Maybe Foo и связанной с ним «головной боли» выяснять, что должно означать неустановленное значение.

Такое поведение особенно вопиюще, потому что оно нарушает закон! Мы ожидаем, что задание msg.foo = msg.foo; не будет работать. Вместо этого реализация фактически втихаря изменяет msg на копию foo с инициализацией нулями, если её раньше не было.

В отличие от скалярных полей, здесь хотя бы можно определить, что поле сообщения не задано. Языковые привязки для протобуферов предлагают что-то вроде сгенерированного метода bool has_foo(). Если оно присутствует, то в случае частого копирования поля сообщения из одного протобуфера в другой необходимо написать следующий код:

if (src.has_foo(src)) {
  dst.set_foo(src.foo());
}

Обратите внимание, что, по крайней мере, в языках со статической типизацией, этот шаблон нельзя абстрагировать из-за номинальной связи между методами foo(), set_foo() и has_foo(). Поскольку все эти функции являются собственными идентификаторами, у нас нет средств для их программной генерации, за исключением макроса препроцессора:

#define COPY_IFF_SET(src, dst, field) \
if (src.has_##field(src)) { \
  dst.set_##field(src.field()); \
}

(но макросы препроцессора запрещены руководством по стилю Google).

Если бы вместо этого все дополнительные поля были реализованы как Maybe, вы смогли бы спокойно поставить абстрагированные точки вызова.

Чтобы сменить тему, поговорим о другом сомнительном решении. Хотя вы можете в протобуферах определить поля oneof, их семантика не соответствует типу сопродукта! Ошибка новичка, парни! Вместо этого вы получаете опциональное поле для каждого случая oneof и магический код в сеттерах, который просто отменит любое другое поле, если это установлено.

На первый взгляд кажется, что это должно быть семантически эквивалентно правильному типу объединения. Но вместо этого мы получаем отвратительный, неописуемый источник ошибок! Когда такое поведение объединяется с незаконной реализацией msg.foo = msg.foo;, такое с виду нормальное присвоение молча удаляет произвольные объёмы данных!

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

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

Ложь обратной и будущей совместимости


Одна из часто упоминаемых «киллер-фич» Protobuffers — их «беспроблемная способность писать обратно- и вперёд-совместимые API». Это утверждение повесили у вас перед глазами, чтобы заслонить правду.

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

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

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

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

Это повсеместное отношение к протобуферам как универсально совместимым проявляется и другими уродливыми способами. Руководства по стилю для Protobuffers активно выступают против DRY и предлагают по возможности встраивать определения в код. Они аргументируют тем, что это позволит в будущем использовать отдельные сообщения, если определения разойдутся. Подчеркну, они предлагают отказаться от 60-летней практики хорошего программирования на всякий случай, вдруг когда-то в будущем вам потребуется что-то изменить.

Корень проблемы в том, что Google объединяет значение данных с их физическим представлением. Когда вы находитесь в масштабе Google, такое имеет смысл. В конце концов, у них есть внутренний инструмент, который сравнивает почасовую оплату программиста с использованием сети, стоимостью хранения X байтов и другими вещами. В отличие от большинства технологических компаний, зарплата программистов — одна из самых маленьких статей расходов Google. Финансово для них имеет смысл тратить время программистов, чтобы сэкономить пару байтов.

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

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

Protobuffers загрязняет кодовые базы


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

Protobuffers соответствуют данным, которые вы хотите отправить по каналу связи. Они часто соответствуют, но не идентичны фактическим данным, с которыми приложение хотело бы работать. Это ставит нас в неудобное положение, необходимо выбирать между одним из трёх плохих вариантов:

  1. Поддерживать отдельный тип, описывающий данные, которые вам действительно нужны, и гарантировать одновременную поддержку обоих типов.
  2. Упаковать полные данные в формат для передачи и использования приложением.
  3. Извлекать полные данные каждый раз, когда они нужны, из краткого формата для передачи.

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

Вместо этого код, использующий протобуферы, позволяет им распространяться по всей кодовой базе. Это реальность. Моим основным проектом в Google был компилятор, который брал «программу», написанную на одной разновидности Protobuffers, и выдавал эквивалентную «программу» на другой. Форматы ввода и вывода достаточно отличались, чтобы их правильные параллельные версии C++ никогда не работали. В результате мой код не мог использовать ни одну из богатых техник написания компиляторов, потому что данные Protobuffers (и сгенерированный код) были слишком жёстким, чтобы сделать с ними что-нибудь интересное.

В результате вместо 50 строк схем рекурсии использовались 10 000 строк специального тасования буфера. Код, который я хотел написать, был буквально невозможен при наличии протобуферов.

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

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

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

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



1. По сей день в Google идёт бурная дискуссия о proto2 и о том, следует ли когда-либо отмечать поля как required. Одновременно распространяются манифесты «optional считается вредным» и «required считается вредным». Удачи разобраться с этим, ребята.
Поделиться публикацией

Похожие публикации

Комментарии 30
    +5
    Понимаю что это перевод, но всё же, зачем выступать против? Выступайте за… что нибудь))) Конечно ProtoBuf развивался стихийно, этим объясняется несовершенство его системы типов.
    Вопрос, а какие есть альтернативы, которые обеспечат такую же статическую типизацию (на основе кодогенерации) и производительность?
      0
      Например Thrift
        +1
        У него все еще хуже и скорость совсем не та, ну по крайней мере было 3 года назад когда мне пришлось очень близко с ним познакомиться.
        И да он больше про RPC чем про эффективную сериализацию.
        0
        thrift?
          0
          Avro?
            +1
            MessagePack от авторов fluentd?
              0
              Нет кодогенерации
                0
                Вы серьезно?))
                  0

                  Смотря для каких целей. И если есть поддержка языков.
                  Гонять данные в/из браузера — отлично.
                  Между системами — есть поддержка множества типов, можно добавить свои,
                  передал дату-время — получил дату-время.


                  Вы серьезно?))

                  А в чем вопрос?

                    0
                    Производительность и типизация — это основная функциональность подобных систем. В Protobuf проделана огромная работа, чтобы выжать производительность до максимума. А вы показываете проектик с 87 коммитами заброшенный больше года назад.

                    А тем кому надо передать дату-время туда-сюда protobuf не нужен.
              +7
              Почему в тэгах XML, если в тексте ни разу не упоминается? Мы должны догадаться, что вместо protobuf автор предлагает использовать XML?
                +21
                Мы вообще живем в мире прототипов которые волевым усилием отправили в продакшен.
                  +1
                  Сериализация в общем и протобуф в частности могут использоваться в разных ситуациях. В некоторых из них важна прямая и обратная совместимость, в других только обратная, где-то совместимость между версиями вообще не нужна.

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


                  В зависимости от приложения и ситуации вариант №1 может оказаться единственно возможным решением, а где-то применим вариант №2 или №3.

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

                  Проблема не в том чтобы написать код сериализации. Написать код сериализации сама по себе простая задача, тем более если весь код заключается в копировании данных из «обыкновенного» класса в сгенерированный протобуфом. Основная проблема сериализации заключается как раз в поддержании совместимости между старыми и новыми версиями классов и данных. И здесь протобуф справляется очень хорошо, пусть и путём ввода некоторых ограничений и фишек вроде required и optional.

                  Например, boost::serialization справляется с задачей обратной совместимости, но поддержки прямой совместимости нет, хотя соответствующие багрепорты открыты давно. Мне пришлось написать специальную библиотеку для того чтобы реализовать прямую совместимость хотя бы отчасти.
                    +1
                    пусть и путём ввода некоторых ограничений и фишек вроде required и optional.
                    Ващет выпилили. Теперь все филды всегда optional.
                      0

                      Required мешает выкидывать поля в новой версии, то есть мешает прежде всего прямой(forward) совместимости. Для обратной совместимости это небольшая проблема. Конечно, для приложений, где не требуется прямая совместимость required был бы полезен.

                    –2
                    Насчёт значений по умолчанию для скаляров это грамотное решение. Опциональность приехала из языков с динамической типизацией, где есть null (да и то не для всех это применимо, явный nil например не может быть значением в Lua таблице). И кроме json я что то не припомню форматов кодирования с explicit null значениями. За опциональность надо платить и пусть это будет выражено в явном виде. Мне кажется со стороны Google это был реверанс в сторону C/C++.
                      +2
                      Критикуешь — предлагай. Какие ещё есть сериализаторы, имеющие биндинги под С, позволяющие гонять данные между ARM32 и 64битной виндозиной, и умеющие не кодировать в пакет поля класса со значением по-умолчанию. Когда выбирал в проект, на последнем пункте срезалось всё, что предлагали коллеги. Но дело было лет 5 тому назад.
                        0
                        ASN.1 BER/DER. BSON. Avro. Thrift. Bycycle в конце концов.
                          0
                          Капитан прото возможно тоже будет неплохим выбором.
                            +1

                            Cap n proto еще в сто раз хуже, там все делается через указатели и можно создавать циклы, с которыми предлагают бороться ограничивая глубину рекурсии.

                          0

                          Есть еще древнючий XDR, но при этом весьма адекватный.
                          https://ru.wikipedia.org/wiki/External_Data_Representation

                          –1

                          Пользую protobuf 2 и 3, как и позднее grpc, много лет в разных проектах на С++, меньше на python. Проблемы автора, признаться, не очень понятны.
                          Поле не может быть repeated? Оберни его в message, и оно сможет, оверхед в С++ будет примерно нулевой.
                          Значения по умолчанию? 0, и ничего другого, RTFM.


                          Не идеально, конечно же, в том же С++ коде профайлер показывает огромное, по сравнению на пример с flatbuffers, количество выделений памяти. Но покажите мне что-нибудь получше, чтоб из коробки понимало хотя бы С++, java, python? Go, С# и Rust желательны, но сейчас не обязательны :)

                            +4

                            Смешаны в кучу претензии к спецификации схемы и к reference implementation кодогенератора. Никто не заставляет использовать protoc от Google.


                            Лично моё мнение:


                            • Заточенность системы типов на типичные случаи использования — это не так уж и плохо. Почти все описанные проблемы решаются обёртыванием в отдельный тип.
                            • Работа с опциональными типами в сгенерированном гугловым компилятором коде действительно ужасна: эти hasFoo() и getFoo() с дефолтными значениями — прямой путь к неожиданному поведению кода вместо вылета NullPointerException. Значение по умолчанию практически никогда не имеет смысла — какие полезные операции можно сделать с объектом, у которого во всех полях нули, пустые строки и вложенные такие же пустые объекты? Это выглядит дико даже в Java, не говоря о языках со встроенными средствами работы с опциональными значениями.
                            • Proto3 пошёл ещё дальше и теперь такая же ситуация в спецификации, так как required полей больше нет. А уж что там происходит с enum — это вообще нонсенс. Не указал значение — получаешь первое объявленное. И нет способа узнать, было ли оно установлено или это "дефолтное". В результате десериализации можно получить всё что угодно — любое поле могло быть не задано и код будет по-тихому работать не так, как задумано.
                              По мне так такая "схема данных" — это просто мусор.

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

                              +2

                              Конкретно по enum решение простое.
                              Объявить первым undefined и появится возможность определить было оно задано или нет

                                +1

                                Да, так и делаем. Но это же ужасно, вам не кажется?

                                  –2

                                  Сложный вопрос.


                                  Как лучше?
                                  Один метод save() или два add() & update()?

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

                              Вот только в protobuf-описании задается именно реализация, а не семантика! Одно из требований к библиотеке для сериализации — это бинарная совместимость с другими библиотеками сериализации работающими с тем же самым форматом.


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


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

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


                              Protobuffers в духе Java различает скалярные типы и типы сообщений. Конечно же, в двух разновидностях типов совершенно разная семантика.

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


                              Но если вы измените foo, он также изменит своего родителя!

                              Обычное поведение ссылочных типов данных в императивных языках. При чем тут вообще protobuf?


                              Мы ожидаем, что задание msg.foo = msg.foo; не будет работать.

                              Так оно и не работает...


                              Обратите внимание, что, по крайней мере, в языках со статической типизацией, этот шаблон нельзя абстрагировать из-за номинальной связи между методами foo(), set_foo() и has_foo()

                              И чем же Reflection и FieldDescriptor — не абстракция? А ведь можно еще и свои кодогенераторы подключать...


                              Чтобы сменить тему, поговорим о другом сомнительном решении. Хотя вы можете в протобуферах определить поля oneof, их семантика не соответствует типу сопродукта! Ошибка новичка, парни! Вместо этого вы получаете опциональное поле для каждого случая oneof и магический код в сеттерах, который просто отменит любое другое поле, если это установлено.

                              Интересно, а какая еще возможна нормальная реализация сопродукта на C++? std::variant, к примеру, при некорректном обращении кидает исключение — то-то радости будет программисту, который не может уследить за тем, какие свойства он читает...

                                +1

                                Нормальная реализация сопродукта делается через деструктурирование и pattern matching, а не через тыкания в стиле dynamic_cast.

                                  0
                                  Вот только последняя версия вышла за два месяца до C++17… Кроме того, старый интерфейс убирать уже нельзя — на него ведь существующий код завязан.

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

                              Самое читаемое