JSON in C++: escaping and serialization
Все, слайды есть.
Всем привет.
Не слышно?
Раз-раз.
Всем привет.
Меня зовут Павел.
Я сегодня хотел бы вам рассказать немножко про Джейсон и про то, как его готовить в C++.
Мы поговорим про гранирование и сериализацию, но на самом деле сейчас мы к этому придем.
Сначала я вам расскажу план выступления, сначала мы расскажу про
Вообще, примерно, что такое JSON, я надеюсь, что все хорошо понимают, что это такое.
Затем мы поговорим про экранирование строк, согласно спецификации, чуть-чуть нырнем глубже в саму спецификацию.
И, скорее всего, нам не хватит времени на сериализацию или стрингифигацию.
К сожалению, я, видимо, откусил больше, чем могу проживать за 40 минут.
Прошу прощения, если кого-то вел заблуждение.
Может быть, в следующий раз расскажу об этом.
Чтобы сделать немножко интересный доклад, мы себя немножко ограничим.
Мы постараемся в рамках экранирования следовать спецификации как можно ближе.
Мы будем использовать C++17, потому что не у всех у нас C++20 или C++23 есть, и мы можем его использовать.
А C++17 вроде бы у всех есть, все смогут такой код использовать.
И мы попытаемся написать как можно меньше кода, потому что там библиотеки на 100 тысяч строк радиопарсинга JSON.
не всегда нужны, и мы попытаемся написать как можно меньше кода.
В этом докладе не будет у нас про парсинг, а также не будет у нас про дизайн типа для работы непосредственно с элементами Gson, элементами документов.
Итак, погнали.
Дуглес Крокфорд предложил джейсон, спецификацию джейсона в начале 2000-х.
Текущий RFC у нас 8259, вот вы видите слева у нас примеры значения джейсона, есть у нас простые значения, их называют спецификация примитивными типами, я называю скалярными типами, есть у нас составные значения типа массива и объекта или словаря,
а спецификациях называют структурные типы, я называю коллекциями.
Почему именно Джейсон?
Я утверждаю, что Джейсон очень простой язык, вот буквально вы можете увидеть всю грамматику целиком, которая помещается на одном слайде.
Не обязательно в этом считываться, мы будем касаться этого.
Для сравнения вот угадайте спецификация чего это.
Вы крикивайте.
Это яму.
Для сравнения вот грамматика C++ целиком, которую я взял из стандарта C++.
Опять же, для сравнения вот RFC, спецификация Json целиком.
То есть, кажется, что действительно Джейсон достаточно простой.
По крайней мере, грамматика Джейсон достаточно простая.
Так, перейдем к экранированию.
Вот увидим такой текст с разными специальными символами, например, переносом строки.
После экранирования превращается в другой текст, где специальные символы и некоторые символы со специальным смыслом
заменяются на экранированные последовательности.
Вот списочек всех символов, которые должны быть экранированы.
Нам обязательно нужно экранировать ковычки обратной слэш и контрольной символы.
Это символы с значениями от 0 до 31.
И, собственно,
коснём спецификация.
Она говорит, что, чтобы наша реализация была переносимая, то есть могла взаимодействовать с другими реализациями, например, на другом компьютере, если мы по сети общаемся, то сериализованное или в текстовом представлении Джейсон-документ должен быть обязательно закатировану UTF-8.
Поэтому мы тоже будем использовать ut8, но мы будем использовать c++17-std-string обычный, но мы будем иметь в виду, что у нас внутри лежит тек закодированный в ut8.
c++20 у нас появился ut8-string, который парметризуется char8t.
И для char8t гарантируется, что в нем может успешно разместиться кодunit ut8, то есть в byte
CodeUnitUTF8, который имеет размер 8-бит, мы всегда сможем положить.
Но в Char, скорее всего, на всех машинах современных и не очень, с которыми вы встретитесь, Char будет иметь тоже размер 8-бит, то есть 8-битный, и для нас это не должно, для нас в связи с этим не должно возникнуть никаких сложностей,
Опять же, мы будем использовать STD String, и даже в C++20 и более поздних стандартах, я бы тоже рекомендовал использовать STD String, потому что возникает вопрос интерперабельности, и не очевидно, как организовать интерперабельность между STD String и STD U8 String.
То есть, может быть, кто-то расскажет, но я таких простых способов не знаю.
Перейдем к самой реализации.
Мы реализуем экранирование с учетом вот таких двух режимов.
У нас будет режим экранирования, собственно, режим помолчания, где мы будем экранировать только обязательные символы к экранированию.
И режим, в котором мы будем экранировать символы, не входящие в STI или ASCII, которые я буду называть, то есть все символы расширены с расширенными кодами за пределами 127.
И так же мы будем, раз мы будем экранировать нязкие символы, почему бы нам ещё не валидировать UT8, и у нас будет выбор валидировать, либо не валидировать, которую мы сможем указать.
Вот такие вот у нас две функции, которые смотрят на пользователь или на которые смотрят сам пользователь.
У нас есть функция
в пространстве имен импл, которая как бы является более продвинутым интерфейсом, который предоставляет все возможности полностью, то есть выставляет наружу в качестве интерфейса.
И есть функция ниже на слайде, которая принимает какой-то текст, принимает режимы икронирования и выдает обратно какую-то строку.
Это как бы более простая воспользование функции, возможно пользователям будет проще воспользоваться ей.
Начнем как раз с нее реализации.
Первым на первым мы проверяем строку на, как это по-русски сказать, на размер ее проверяем, если она не пустая, если она, наоборот, пустая, то мы возвращаем пустую строку, потому что экранированная пустая строка и пустая строка.
В противном случае, если строка не пустая, то мы проверяем режим, если у нас не заказано
Валядация эту мы знаем, что мы не можем прекратить работу из-за ошибки, поэтому мы какой-то результат в итоге всё равно выдадим.
И так как экранированная строка всегда больше или равна по размеру исходной строке, то мы можем сэкономить несколько локаций памяти и сделать резерв.
Далее у нас идёт вот такой вот новый для данного доклада, по крайней мере.
может быть не такой новый для всех слушателей концепт, как сток или синк.
Это некая сущность, у которой есть оператор вызов функции, то есть это может быть функтор, либо функция, в которой мы будем передавать кусочки уже обработанного входа, то есть кусочки экранированной строки и предполагается, что этот сток будет каким-то образом тоже либо запоминать, складывать в строку,
все кусочки, в итоге у нас получится полностью экранированная строка, либо может быть он предпочтет пользователь, может предпочтечь сразу выводить результат в консоль, например.
И в итоге вот такой вот экземпляр нашего стока мы передаем в нашу функцию с более продвинутым интерфейсом вместе с исходным текстом и с режимной работой.
Можете видеть здесь, что наша продвинутая функция возвращает какое-то значение.
Это размер обработанного входного текста.
То есть если у нас функция возвращает равный размер исходного текста, значит полностью весь исходный текст обработали, значит у нас все прошло хорошо, ошибок не возникло, мы можем вернуть результат.
Если у нас, собственно, возникла какая-то ошибка, то мы провалим и дальше возвращаем пустую строку, тем самым сигнализирую, что у нас произошла какая-то ошибка.
И вот таким вот образом можно использовать эту функцию.
Далее у нас мы пошли внутрь с какой-то слои абстракций, и это у нас уже функция с более продвинутым интерфейсом, который представляет более продвинутый интерфейс.
Здесь мы создаем вспомогательный объект типа escapeStringWriter.
И, собственно, передаем ему туда объект stock, который нам передали.
И, собственно, вызываем метод write этого вспомогательного объекта.
И таким образом предполагается, что в stock
Все кусочки или, может быть, один большой кусок нашего входного текста, на который указывают указатели, которые мы получаем из дата и дата плюс сайс, мы в итоге передадим.
И в итоге мы, как результат, возвращаем в собственный размер обработанной строки, как разность указателей
окончательного, на котором остановили обработку и начального, который мы получаем из дата.
Это все скучно.
Давайте нырнем еще на один уровень абстракции.
Это как раз нас помогает на объект.
Здесь у нас есть конструктор.
Все вроде бы просто.
Мы запоминаем ссылку на объект SYNC.
Есть функция write, которую мы как раз реализуем.
И есть какие-то приватные данные.
Тут ссылка на объект SYNC.
Есть
еще один вспомогательный объект типа эпчарка, на который мы тоже посмотрим.
Далее есть указатель на пенник Бигин.
Пенник Бигин — это указатель на начало кусочка, который мы просматриваем, но еще не записали.
Вот вы можете справа видеть крупно, собственно, внутренний
тело-функция без тела внутреннего цикла.
Здесь мы назначаем указатель на начало пендик-бегин и начинаем цикл.
Пока у нас пендик-бегин не равен окончанию, мы крутимся во внешнем цикле.
Указатель и это у нас указатель на текущий символ, на который мы смотрим.
И, собственно, во внутреннем цикле мы просматриваем символы один за другим.
Вот справа вверху можете видеть указатели, как я обозначил.
и давайте начнем просматривать.
Вот мы, значит, просматривали, просматривали, просматривали, встретили специальный символ.
У этого специального символа есть короткая последовательность для экранирования, поэтому мы проваливаемся в наши первые условия.
Здесь мы вызываем функцию match command character escape, character to escape, передаем ему какой-то символ.
Если мы обратно получили уже экранированную последовательность нолевого размера, значит,
Для этого символа есть соответствующая короткая последовательность, и мы ее записываем.
Собственно, вот здесь вот мы, да, вот я забыл, что я вывел крупно это на слайд.
Вот вы можете видеть крупнее, что здесь мы зовем h.com.nsk, проверяем условия.
Если условие у нас сработало, то мы вызываем write.and.advance.
Собственно вот наша функция мачкоман, character.escape.
Здесь мы используем switch, здесь все просто.
Почему я использую switch?
Потому что у компиляторов есть тенденция превращать switch в таблицу прыжков, поэтому везде, где я мог, я постарался использовать switch.
И вот, собственно, мы возвратились из этой функции, получили какой-то последовательность для этого
символа.
У нас проверка условия показала, что этот символ не пустой, этот последовательность не пустая.
Значит, мы должны записать все, что мы просмотрели до этого символа и, собственно, саму последовательность для этого символа тоже записать.
Вот таким вот образом мы делаем, если у нас условие говорить, что мы что-то просмотрели, но не записали,
Значит, нам нужно это записать.
Вот тот кусочек вы можете справа вверху увидеть.
Я обознател, который мы просмотрели на диспеталь.
Мы его записываем.
Затем мы переставляем указатель начала просмотра на следующий символ.
И, собственно, последней строчкой мы записываем в наш сток ту последовательность, которую нам выдал наш функция для просматривания символа.
И вот, собственно, вот таким вот образом этот кусочек кода работает.
Давайте просматривать дальше.
Значит, мы просматривали, просматривали, встретили другой специальный символ, на этот раз контрольный символ, для которого нет какой-то короткой последовательности предусмотренной экранирования.
В этом случае вот такой вот код у нас работает.
Здесь мы проверяем условия, что, если
Это, собственно, контрольный символ, то есть у него старшие битики, старших несколько битов равны нулю, значит, это контрольный символ, и мы записываем последовательность для этого символа, которому получаем с помощью вызова метода get у нашего вспомогательного объекта escape.
После этого, после того, как мы записали, мы переставляем указатели, возвращаемся, возвращаемся, собственно, в начало
в нашего внешнего цикла у нас происходит в данном случае проверка, что указатель на начало рен, указатель на окончание, и мы, собственно, дальше выходим и возвращаем результат.
Как работает наш вспомогательный объект?
У него есть метод GET, с помощью которого мы превращаем код символа в некую последовательность.
И, собственно,
Мы используем вот этот приватный метод статический, чтобы записать в наш буфер обратный слаж u и 16-ти личное значение кода, который нам предоставили.
Кстати, не стоит вот так вот делать, это я просто сделал, чтобы можно было поговорить об этом на слайде.
И вот справа вы можете видеть, что для нулевого символа мы получим вот такой вот обратный слаж u и 4 нуля.
Может быть другая ситуация, что мы просматривали и просматривали вот таким вот образом.
И у нас на самом деле срабатывает вот этот кусочек кода, где мы инкрементируем указательный текущий символ, мы просматривали и просматривали, дошли до конца до окончания строки.
В этом случае у нас здесь есть еще одна проверка, которая это проверяет, ловит.
И при ее срабатывании мы опять же записываем то, что мы успели просмотреть, но не записали.
Вот справа показано этот кусочек, который должен быть записан.
Записываем его и возвращаемся из функции.
Зашибись.
Мы реализовали какую-то
часть, но у нас осталось нереализованное иканирование неоски символов и валидация.
Это самое, на мой взгляд, интересное.
Начнем с короткого ликбеза про UTF-8.
Значит, на мой взгляд, гениальная кодировка придуманная.
Здесь у нас есть вот таким вот образом заданы префиксы, которые определяют, что это у нас за код-юнит или что это у нас за байтик в данном случае.
Если начинается с нуля, то это у нас обычный Ascii Symbol.
Если начинается с единицы нуля для старших битов, то это значит у нас байт продолжения.
Если начинается с двух единицы нуля, значит это байт стартующий двух, байтовая последовательность, ну и так далее.
Три единицы нуля и ноль.
Это три байта последовательность, четыре единицы ноль.
Это четыре байта последовательность, вот такая вот мемоника.
И вот таким вот образом мы можем закодировать наши разные значения, наши разные символы.
Например, для знака доллара у нас достаточно одного байта или одного код-юнета.
И вот таким вот образом просто байтики переезжают в закодированный вид.
Для знака фунта нам не хватает одного байтика, нам нужно два байтика уже.
И вот таким вот образом вы можете видеть, как разные биты, разные части этого двоечного представления переезжают в соответствующий стартовый байт или код-юнит и в байт продолжения.
Ну и, соответственно, для евро нам нужно уже три байта или три код-юнета и вот таким вот образом
битики переезжают и соответственно вот в данном случае для этого и модзии нам не хватает трех байтов и нам нужно уже четыре байта использовать и соответствующим образом битики переезжают вот байта продолжения так как у нас как по сути есть ведущие 3 нуля допустим который я здесь не показал у нас они просто остаются нулями в стартующем байте
Давайте теперь это реализуем.
Перво-наперго нам нужно проверить, является ли символ, на который мы сейчас смотрим, расширенным символом, то есть не оски символом.
Это мы можем сделать, проверив старший битик, если он единица, значит это наш случай, если он не единица, значит мы как бы проваливаемся дальше и нам ничего специального делать не нужно.
Да, опять я забыл, что здесь я в увеличенном виде показываю.
Вот, собственно, проверка.
Дальше мы считаем, сколько, какое количество байтов или код юнитов должен занимать эту последовательность, которую мы смотрим, точнее, начало, которое мы смотрим.
Дальше, если у нас заказана валидация, мы проверяем, что если у нас
вернулся неволидный размер, то есть по какой-то причине у нас что-то пошло не так, с входным текстом и там что-то не срослось, то мы соответственно возвращаем при этом ошибку, то есть нам заказали
возвращать ошибку или заканчивать обработку при завершении, при неволидном UTF-8 и, собственно, мы возвращаем указатель на начало последовательства, где мы обнаружили ошибку.
В противном случае, если у нас всё хорошо, ошибки не произошло, то мы увеличиваем указатель на текущий символ на то количество, на размер нашей последовательности UTF-8,
То есть нам не нужно следующие символы просматривать, потому что мы, по сути, их уже просмотрели.
Дальше у нас идёт GoTo.
И почему здесь GoTo?
Потому что мы сейчас добавим коды ещё, и чтобы избежать дубликации в разных частях одного и того же кода, то здесь проще просто поставить GoTo.
Код станет немножко понятнее без дубликации и, к сожалению, другого способа
Я не нашел это сделать кроме ГОТУ, но иногда и ГОТУ оказывается полезным.
И, собственно, если наша проверка не сработала, то есть у нас есть обычно тутасский символ, мы просто не выполняем все, что у нас под условием и перемещаемся сюда и инкрементируем наш указатель на текущий символ.
Давайте теперь посмотрим, как реализуют стекодирование.
Вот пример квинтэссенциальный, пример того, как это делается в интернетах.
У меня с этим к этому примеру кода такая проблема.
Такой вопрос, что, чтобы удостовериться, вот если я нашел, например, в интернете, я себе с копипастом, чтобы удостовериться, что он работает правильно, мне нужно разобраться со всеми этими константами и, по сути, мне нужно пере...
В общем, запилить практически заново всю эту функцию.
И, собственно, откуда эти константы, что они, значит, не очевидны.
Поэтому мы так делать не будем и сделаем немножко по-другому.
Вот вверху у нас есть функция, которая проверяет, является ли данный символ или данный код-юнит каким-то специальным
имеет ли специальное значение?
Здесь у нас есть маска, которую мы вычисляем, есть шаблон-паттерн, который мы тоже вычисляем, и потом проверяем, является ли у нас замаскированные биты равны нашему шаблону.
Например, вот внизу для размера 3, для n равно 3, маска у нас получается 4 старшей бита в единицу выставлена, и шаблон
Получается, 3 старших битов единицы выставлены, и дальше идёт 0.
То есть мы проверяем, что старшие 4 бита имеют ровно такие значения, 4 единицы и 0.
И отработает для любых значений от 0 до 4, и может показать, какой из специальных код-юнитов или какой шаблон код-юнита в данном код-юните лежит.
И теперь мы можем наконец посчитать и убедиться, что наша последовательность CodePoint, которая состоит из CodeUnits, корректной.
Вот вверху у нас функция, которая вычисляет предполагаемый размер CodePoint, которая может стать из нескольких CodeUnits.
Здесь мы проверяем только на размеры 2.3.4, потому что мы находимся в точке кода, где
Мы точно знаем, что это не размер 0 и мы не предполагаем, что это может быть continuation byte, потому что это была бы ошибка.
Поэтому мы проверяем только размеры 234.
И вот следующая функция как раз вычисляется и проверяет весь codepoint.
все подъюнеты, которые в него входят, мы получаем предполагаемый размер.
Дальше мы проверяем, а да, вот здесь я не сказал, что мы возвращаем единицу, потому что по разным причинам нам удобнее, чтобы это было именно единица была неволитным значением.
И дальше
Мы проверяем, что у нас предполагаемый размер не является неволидным, то есть у нас проверка стартующего код-юнита прошла успешно.
Дальше мы вычисляем, что у нас в нашем диапазоне между Begin and End есть место, чтобы туда как бы поместился заданного размера код-юнит, код-поин, точнее, то есть все код-юниты.
И последняя строчка в условии, мы проверяем, что все следующие байт или код-юниты у нас являются
байтами продолжения или continuation bytes.
Если у нас все проверки прошли, мы возвращаем, собственно, предполагаемое значение, которое мы подтвердили, что оно такое есть.
В противном случае, если у нас что-то пошло не так, мы возвращаем в данном случае уже ноль, как неволивное значение.
И, собственно, вот мы таким образом реализовали валидацию, нам осталось реализовать
кронирование неоски символов.
Здесь, в принципе, heavy lifting, так сказать, мы уже сделали, нам осталось просто взять уже с посчитанным размером код-поинта и взять соответствующие последовательность для икронирования и заслать ее в stock, липсинг.
Здесь вы можете видеть, что в непосредственно вызованной лямбе мы
используем опять же switch, с default не обращать внимания, это чтобы компилятор не ругался.
Для размера 2 и 3 мы здесь вызываем собственно gather bits, еще одну функцию на которую мы посмотрим и вызываем метод get у нашего вспомогательного объекта escape.
Здесь, в принципе, ничего нового кроме gather bits, которые я думаю вы предполагаете что делает
И новая здесь для размера четырех метод GetSurrogate, на которой мы сейчас посмотрим.
Опять же, чтобы декодировать какой-то кодпоинт, в данном случае, чтобы декодировать кодпоинт из четырех коддюнентов,
можно пойти в интернет и найти такой квинтэссенциальный пример реализации.
Опять же, у меня лично к этому коду такой же вопрос, что здесь есть какие-то константы.
они не очевидны их значения, и чтобы удостовериться, что реализация правильная, мне, по сути, нужно заново пройтись и реализовать функционал, который мне здесь необходим.
Таким образом, я удостоверюсь, что все константы были выведены правильно.
Мы опять же так делать не будем, мы сделаем вот таким вот образом.
Мы определим константу лову бит с маск.
Очень полезная константа.
У нее все биты, младших битов, выставленные в единицу, остальные биты выставлены в ноль.
И, опять же, давайте, чтобы особо не вчитываться в код, сразу разберем на примере.
Например, для размера трех маска получается у нас вот таким вот образом.
и далее у нас есть функция gatherBits, которая собирает битики из разных код-юнитов или байтов, которые входят в код-поинт.
И опять же, давайте на примере, здесь у нас для размера 3, то есть мы берем 3 бита из первого байта, из 666, из остальных байтов,
И в качестве примера, это здесь не играет большой роли, давайте предположим, что у нас в первом байтом лежит вот такое значение.
Это значение стартующая четырехбайтовый код Point.
Дальше мы проверяем, что все битики, которые нам заказали, должны уместиться в целочисленное значение, которое также тип указывается.
Если это не так, то, значит, что-то пошло не так, либо с типом целочистного значения пользователь ошибся, то есть мы ошиблись как реализаторы, которые пользователи этой функции, либо что-то не так пошло с размерами питиков, которые нам нужно достать с каждого байта.
Далее мы маскируем значение из нашего первого байта с помощью Константа Лоубиц Маск.
Далее, если... А, да, и вот результат эквалентин, такому выражению.
Далее,
если у нас больше никаких размеров не заказано, значит, результат готов, мы все сделаем, если что нужно и возвращаем результат.
Опять же, в нашем примере у нас есть другие размеры, которые нам заказали.
Это, знаете, что те битики, которые мы уже положили в результат, нам нужно сместить и освободить место для остальных битиков из других битов.
И, собственно, в данном случае вот таким вот образом на 18 битов
мы должны сместить наши битики, которые мы уже положили, и, собственно, мы рекурсивно вызываем сами себя с функцией GetherBits и передаем остальные размеры битов, которые нам нужно получить, и остальные указатели на остальные байты.
Теперь перейдем к суррагатам.
Собственно, вот у нас есть значения, которые у нас могут быть закодированы четырехбаттой последовательностью.
И диапазон этих значений можно закодировать от 17 до 21 бита.
Но вот тот формат экранирования, который нам позволяет JSON, он может закодировать максимум 16 битов.
Что делать?
За нас, как всегда, уже все было сделано в Unicode.
Есть UTF-16, есть UTF-16 суррогат и, собственно, ими мы и воспользуем.
Что такое UTF-16 суррогата?
Это пара UTF-16-код-юнитов, которые, можно сказать, образуют код-поинт.
И у нас есть так называемый старший суррогат или HIGH-суррогат, который начинается с 1.1.0.1.1.0.
в него попадают старшие биты из нашего значения, которые мы хотим закодировать.
Есть младший суррогат или лов-суррогат, у которого начало 1.1.0.1.1.1, в который попадают младшие биты из того значения, который мы хотим закодировать.
И в качестве примера, опять же, эмодзии, которые не влезают в
16 битов, и вот таким вот образом старшие биты и младшие биты попадают в соответствующие суррогаты 16-битовые UTF-16 код-юнеты.
И возвращаемся опять к нашему вспомогательному объекту.
Внимательные зрители могли увидеть, что мы здесь для нашего буфера заказали 12 символов.
и это было не случайно, потому что как раз, когда мы экранируем суррогатную пару, мы хотим это сделать сразу пачкой, и мы это можем сделать, положив в 12, в буфер на 12 символа, то есть у нас Slash U, 4, 16-тиричные значения, и еще раз Slash U, 16-тиричные значения.
И вот таким вот образом,
Мы это реализуем, опять же, внимательные слушатели могли заметить, что у нас само значение символа, диапазон значения 17 битов до 21 бита, а суррогатная пара может вместить только 20 битов, то есть 10 битов старшим и 10 битов младшим.
Что делать?
Вот такое техническое решение было придумано, что мы вычитаем из значения нашего символа вот такое значение 16-ти речи на 1.000.
и это приводит как раз диапазон необходимых значений к диапазону от 1 до 20 битов.
И 20 битов мы уже можем положить в срагатную пару.
И вот таким образом мы это делаем и вызываем приватную функцию write и
Просим записать в начало буфера старшие биты и вторым вызовом.
В вторую часть буфера мы просим записать младшие биты из нашего значения.
Ну и, собственно, возвращаем стринг-view на наш буфер.
И для примера вот наша моди-символ будет закодируем вот такой вот суррогатный парой.
Собственно, мы всё реализовались, что хотели, у нас остается небольшой нюанс с производительностью.
Дело в том, что вот эта проверка относительно дорогая, если пользователь, который нас просит что-то экранировать,
не просит нас не проверять валидность UT8, он не просит нас кодировать не ASCII символы, потому что у него канал передача позволяет, то есть нет ограничения на канал по передаче только ASCII символов, поэтому никаких дополнительных телодвижений от нас делать не просят, но вот эта проверка, она будет относительно дорогая.
в случае, когда всё у нас по умолчанию, весь всё поведение по умолчанию.
Что можно сделать?
Можно вот такое вот перечисление сделать, где сделать разные комбинации режимов, режим по умолчанию, экранировать неоски, валядаться и одновременно экранировать неоски и валидировать.
Нашу функцию, которую написали Riot, мы можем приместить в приватную часть,
Сделать из нее шаблон, переименовать.
И в той части, где у нас как раз идет обработка UTF-специфических, UTF-8-специфических поведения, мы можем все это обернуть в constexpr.
Вот таким вот образом тогда, если у нас режим не является режимом по умолчанию, то, собственно, сама проверка, что это какой-то специфический UTF-8-символ будет выполняться только в этом случае.
Если у нас все по умолчанию, то эта проверка просто даже не будет скомпилирована.
И, собственно, если у нас, если нас просят экранировать несколькие символы, то опять же этот код будет включен только в этом случае.
Если опять же при этом нас еще попросят валидировать, то опять же мы, с помощью и в констэкспор, включим соответствующий код только в этом случае.
В противном случае, если мы сюда попали, это значит, что у нас режим не по умолчанию, но у нас не просили экранировать неоские символы, значит, у нас остается validation.
Нам нужно здесь в этом else оставить код validation.
Собственно, раз мы переместили нашу функцию write, мы должны новую функцию write написать, она здесь
Достаточно тривиально, несмотря на то, что здесь много кода.
Здесь мы в runtime уже проверяем наши режимы и вызываем соответствующую специализацию функции write-in-poll.
Например, для режима помолчания в зависимости от режима validation
вызываем спецификацию либо validateUTF8, либо default.
В противном случае, если нас просят экронировать, в зависимости от validations мы вызываем соответствующие специализации нашей функции.
И, собственно, наконец-то мы можем написать вот такой код, пример использования, мы получим вот такой вот вывод, как внизу слайда, в разных режимах.
И на этом у меня всё на данный момент.
Откуда у меня есть буквально три минуты для того, чтобы рассказать, откуда вообще ноги растут у этого доклада.
Мне было нечего делать, я прокрастинировал.
Я не уверен, что когда прокрастинируешь, это нечего делать.
Не важно.
Я решил себе поставить задачу написать Джейсон библиотеку как можно меньше строк-кода.
У меня получилось 1500 строк-кода, вы можете найти на гитхабе, либо вы можете
В любом поисковике забить меньше солнца ПП и найдете в этой библиотеке уже условно боевой код того всего, что я вам рассказываю даже больше.
И собственно все, ссылка на слайды, QR-код на слайды, и у нас есть где-то 12-минутные вопросы.
Большое спасибо за доклад.
Тут просят вернуть пример использования.
Пока на него и будет, задаю вопросы.
Кажется, ассамблевная операция вычисления старшего значчивого бита позволяет вычислить правильный код по инсайс за пару инструкций, чтобы быстрее серии есть, плюс рейтен.
Отличный вопрос.
Я пробовал использовать эту инструкцию, у меня получилось медленнее.
Потому что это БСР, там или Каунт Лзера, или что-то в этом роде инструкция.
Это на самом деле взятие логорифма, логорифм относительно медленной операции.
В итоге у меня получилось, в моем случае, возможно, в вашем случае будет что-то другое, что там три Ивчика, они получаются быстрее, чем взять вот этот вот поиск первого ненуливого бита или первого нуливого бита, или взять логорифма, чем
То есть, ифы получились быстрее в моём случае, чем вот такая инструкция.
Хороший вопрос.
Как эффективно парсить строки, которые скорее всего содержат Json, но сами могут им не являться?
Вопрос не по докладу.
Я вначале говорил, что про парсинг ничего не будет, но дайте подумать.
Как эффективно парсить строку, которая, возможно, Json, но, возможно, нет?
Но я бы сказал так, если вопрос в скорости или в задержке, то я бы взял SIM-JSON и попытался с помощью него распарсить строку, в которой возможно JSON.
И мне бы SIM-JSON сказал, что у тебя либо парсинг прошёл успешно, либо может быть надо такинуть данных, либо что-то пошло не так совсем.
Лично у нас есть
Провокационные вопросы, конечно.
Я их два объединю в один.
Нет ли уже готовой реализации такой популярной задачи, как гранирование строки до какого-нибудь ассамблере, которая будет работать быстрее, чем на C++?
И второй туда же.
Неужели никто не реализовал какой-нибудь симпт Джейсон?
Собственно, ответ уже есть в вопросе.
Мне нужно что-то добавлять.
Я немножко добавлю.
Я, опять же, в начале сказал, что ноги растут оттуда, что я использовал C++17 и я старался написать как можно меньше кода.
Понятно, что когда, например, у нас, если бы сейчас уже была доступная Stadosim, какой-нибудь в стандарте, этот стандарт был бы широко
распространён, то, возможно, что-то в этом роде можно было бы сделать.
А тогда Сим Джиссон – это лучший вариант из тех, что я знаю.
Библиотека итоговая.
Получилось Хедер-Онлих?
Хедер-Онлих, да.
Гонялись ли беншмарки, сравнивали производительные с существующими аналдами?
Гонялись, сравнивались.
скажем так, проигрывает некоторым даже не сим-джисона она проигрывает примерно в 10 раз порядка 10 раз она проигрывает некоторым другим библиотеком но она ее производительность одного порядка но преимущество на мой взгляд в том что там только 1500 строк вы можете пойти там взять кусочек кусок кода который вам только он интересует вместо того чтобы тащить библиотеку например на 100 тысяч строк или там на
50 тысяч строк или на 30 тысяч строк.
Вот так трейдов, компромисс.
Дает ли преимущество в скорости вписывания логики Inline в нужное место кода, вместо вызова отдельной функции из нескольких строк?
Хороший вопрос, мне кажется это кейс-байкейс, то есть в каждом конкретном случае нужно смотреть.
в целом я бы с этим не заморачивался и более того это наверное даже зависит то есть то что вы напишите онлайн ни на что особо не влияет или вопрос был если логику писать онлайн или выносить функцию смотрите компилятору в целом я скажу про онлайн если вы будете везде писать онлайн кроме того что
это влияет на ОДР, правило, компилятору по барабану он будет принимать скорее всего решение самостоятельно, не будет смотреть на ключевое слово «инлайн», встраивать какую-то конкретную функцию в конкретном месте или не встраивать.
По поводу вопроса, писать ли функцию на 1500 строк или можно разбить эту функцию на несколько функций,
Мне кажется тоже нужно смотреть конкретные случаи, потому что, ну, наверное, никто не любит функции там на тысяче строк, на десятки экранов.
Поэтому я бы ориентировался скорее на поддержимость, то есть ментайнабилити, на кодстайл, на какие-то такие общечеловеческие вещи, а не сразу... То есть не дел-бы примочую оптимизацию, не пытался бы делать.
Вопрос.
Я правильно понимаю, что здесь только про экранирование строки перед тем, как добавить ее в JSON?
Да, правильно.
Когда мы сериализуем JSON, то в сериализованном JSON строки должны быть экранированы.
И, собственно, в данном докладе про это, собственно, и было.
Ну и, похоже, мы прошлись по всем вопросам.
У меня буквально 5 минут есть, давайте я вам покажу чуть-чуть интересного из сериализации, кусочек интересного из сериализации.
Это у нас про строки, про ошибки.
Смотрите, мы здесь серизуем объект для пустого объекта здесь.
ничего интересного нету, интерес начинается, когда мы сортируем не пустой объект.
Если у нас в объекте один намбер, тут опять же ничего интересного нету, интерес начинается, когда несколько намберов.
У нас есть возможность отсортировать по ключам, либо не сортировать.
Когда мы не сортируем, а, ну, здесь мы закачиваемся реализацию.
Когда мы не сортируем, то здесь, в принципе, все просто, здесь мы
Давайте вот так вот покажу.
Мы просто проходимся по сути по итераторам, которые нам дали и сервизуем все мемборы.
Что делать, интересно, начинается, да, когда... Что делать, когда нужна сортировка?
То есть у нас вот... Мы храним наши пары ключеноченьев в несортированном AnonDreadMap, то есть там, неупорядоченном.
Но если пользователь хочет, чтобы
эти мемборы были отсортированы по ключам, вопрос что делать.
Мы уже написали функцию WriteObjectMembers, она хорошая, мы не хотели бы писать какой-то дополнительный код отдельно, ещё отдельную функцию, чтобы учесть другой тип контейнер или как-то учесть сортировку.
Поэтому я предлагаю сделать вот таким вот образом.
Мы можем создать референс рэппер, алиаст для вот такого вот STD референс рэппер, на VLG type нашего собственно AnodeRedMap.
Далее мы можем создать вектор
собственно вот этих вот референцев, и вот так совпадает божественно, что мы можем его инициализировать, просто передав исходные, показать итераторы Big End, и у нас внутри этого контейнер создаются объекты типа Reference Rapper, которые на самом деле указывают на объекты в нашем исходном анодород-мэп.
И далее мы вот таким вот образом
передаем в эстедесорт лямду, внутри которой мы обратно делаем unrepp, разворачиваем наши символы.
Элементы у этого элемента мы берем, как бы это вот внутренний гет.
Наружный гет 0, он как бы берет из этой пары ключ, сравнивает по ключам.
И вот таким вот образом мы можем отсортировать неупорядочный словарь, а нодротмэп без копирования элементов в другой контейнер.
То есть нам достаточно только
скопировать, точнее создать контейнер с reference wrapper, который на самом деле что-то вроде указателя и размером с указатель.
И вот такой относительно малой крови мы можем вот этого достичь.
Ну а дальше мы просто нашу уже существующую функцию write object members вызовом и передадим уже итераторы на контейнер с reference wrapper, а там внутри уже магически все получается само собой.
Всё, да.
У меня есть буквально минута, не знаю.
Давайте ещё интересный покажу.
Ранее на слайдах я показывал, что мы вот такие вот... Когда мы сервизуем, мы получаем вот такую строку, но мы можем сделать перенос строки, а можем каждый вот этот вот специальный кусочек, типа открывающей скобочки или кавычки перед назначить.
Вот мне, например, нравится здесь вот... А, да, пример оцертированных.
ключей здесь практически по умолчанию, но вот мне нравится лично, когда после двойточки идет пробел.
Мы это можем сделать вот таким вот образом легко.
На этом мы можем не останавливаться, мы можем вот таким вот образом переназначить вообще все кавычки, скобочки и так далее.
Это Terminal Escape Sequences или, как сказать, я не знаю,
экранирующей последовательности для терминала или для консоли и таким вот образом мы можем в консоли спечатать уже с цветом.
Или мы можем вообще сделать это элементами HTML и получить на выходе вот такой вот HTML-ник.
Мы можем обернуть его в какой-нибудь типа прее и получить вот такой вот атрендерий на HTML.
И вот теперь действительно все.
Я даже положился
Спасибо за паспорт!