C++ Unicode: байты, кодовые точки и графемы

Итак, я создаю язык сценариев, и одна из моих целей - это удобные строковые операции. Я попробовал некоторые идеи в C++.

  • Строка как последовательность байтов и свободных функций, которые возвращают векторы, содержащие индексы кодовых точек.
  • Класс-обертка, который объединяет строку и вектор, содержащий индексы.

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

В итоге я создал класс-оболочку вокруг массива char, равного 4 байтам: строка, в памяти которой ровно 4 байта, не больше и не меньше.

Создав этот класс, я испытал желание просто обернуть его в std::vector из этого в другом классе и построить оттуда, таким образом, делая строковый тип кодовых точек. Я не знаю, хороший ли это подход, в конечном итоге он будет намного удобнее, но в итоге будет тратить больше места.

Итак, перед публикацией некоторого кода приведу более организованный список идей.

  • Мой символьный тип был бы не байтом или графемой, а скорее кодовой точкой. Я назвал ее руной, подобной той, что на языке го.
  • Строка как серия разложенных рун, что делает индексацию и нарезку O1.
  • Поскольку руна теперь является классом, а не примитивом, она может быть расширена с помощью методов обнаружения пробела в Юникоде: mysring[0].is_whitespace()
  • Я до сих пор не знаю, как обращаться с графемами.

Любопытный факт! При создании прототипа класса рун странным было то, что он всегда печатается в UTF8. Поскольку моя руна - это не int32, а 4-байтовая строка, в результате получаются некоторые интересные свойства.

Мой код:

class rune {
    char data[4] {};
public:
    rune(char c) {
        data[0] = c;
    }

    // This constructor needs a string, a position and an offset!
    rune(std::string const & s, size_t p, size_t n) {
        for (size_t i = 0; i < n; ++i) {
            data[i] = s[p + i];
        }
    }

    void swap(rune & other) {
        rune t = *this;
        *this = other;
        other = t;
    }

    // Output as UTF8!
    friend std::ostream & operator <<(std::ostream & output, rune input) {
        for (size_t i = 0; i < 4; ++i) {
            if (input.data[i] == '\0') {
                return output;
            }
            output << input.data[i];
        }
        return output;
    }
};

Идеи обработки ошибок:

Я не люблю использовать исключения в C++. Моя идея заключается в том, что, если конструктор не удается, инициализировать руну как 4 '\0', а затем явно перегрузите оператор bool, чтобы вернуть false, если первый байт цикла был '\0', Легко и просто использовать.

Итак, мысли? Мнения? Разные подходы?

Даже если моя руническая строка слишком большая, по крайней мере, у меня есть рунический тип. Маленький и быстрый для копирования.:)

1 ответ

Похоже, вы пытаетесь изобрести велосипед.

Есть, конечно, два способа думать о тексте:

  • Как массив кодовых точек
  • Как закодированный массив байтов.

В некоторых кодовых базах эти два представления одинаковы (а все кодировки в основном являются массивами char32_t или же unsigned int). В некоторых (я склонен сказать "большинство", но не цитируйте меня об этом), кодированный массив байтов будет использовать UTF-8, где кодовые точки преобразуются в переменные длины байтов перед помещением в структуру данных,

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

Для ваших целей, хотя имеет смысл написать класс для "обтекания" ваших данных (хотя я бы не назвал это runeЯ бы просто назвал это codepoint), вы захотите подумать о своей семантике.

  • Вы можете (и, вероятно, должны) лечить все std::stringЭто как строки в кодировке UTF-8, и вы предпочитаете этот интерфейс по умолчанию для работы с текстом. Это безопасно для большинства внешних интерфейсов - единственный раз, когда он потерпит неудачу, это когда он взаимодействует со входом UTF-16, и вы можете написать угловые случаи для этого - и это сэкономит вам больше памяти, при этом соблюдая общие строковые соглашения (это лексикографически сопоставимый, который является большим).
  • Если вам нужно работать с вашими данными в форме кодов, вам нужно написать структуру (или класс) с именем codepointсо следующими полезными функциями и конструкторами
    • Хотя мне приходилось писать код, который обрабатывает текст в форме кодов (особенно для средства визуализации шрифтов), вероятно, это не то, как вы должны хранить свой текст. Сохранение текста в виде кодовых точек приводит к проблемам позже, когда вы постоянно сравниваете со строками в кодировке UTF-8 или ASCII.

код:

struct codepoint {
    char32_t val;
    codepoint(char32_t _val = 0) : val(_val) {}
    codepoint(std::string const& s);
    codepoint(std::string::const_iterator begin, std::string::const_iterator end);
    //I don't know the UTF-8→codepoint conversion off-hand. There are lots of places
    //online that show how to do this

    std::string to_utf8() const;
    //Again, look up an algorithm. They're not *too* complicated.
    void append_to_string_as_utf8(std::string & s) const;
    //This might be more performant if you're trying to reduce how many dynamic memory 
    //allocations you're making.

    //codepoint(std::wstring const& s);
    //std::wstring to_utf16() const;
    //void append_to_string_as_utf16(std::wstring & s) const;

    //Anything else you need, equality operator, comparison operator, etc.
};
Другие вопросы по тегам