Работа со строками на этапе компиляции в современном C++


    Если вы программируете на C++, то наверняка задавались вопросом почему нельзя сравнить два строковых литерала или выполнить их конкатенацию:


    auto str = "hello" + "world"; // ошибка компиляции
    
    if ("hello" < "world") { // компилируется, но работает не так, как ожидалось
        // ...
    }

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


    Зачем все это нужно


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


    // plugin.h
    
    const std::string PLUGIN_PATH = "/usr/local/lib/project/plugins/";
    
    // ...

    // sample_plugin.h
    
    const std::string SAMPLE_PLUGIN_LIB = PLUGIN_PATH + "sample.so";
    
    // ...

    Думаю, вы уже догадались, что случилось в один прекрасный день. SAMPLE_PLUGIN_PATH приняла значение "sample.so", несмотря на то, что PLUGIN_PATH имела значение "/usr/local/lib/project/plugins/", как и ожидалось. Как это могло произойти? Все очень просто, порядок инициализации глобальных объектов не определен, в момент инициализации SAMPLE_PLUGIN_PATH переменная PLUGIN_PATH была пуста.


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


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


    В этой статье рассмотрим строки, операции над которыми можно проводить на этапе компиляции. Назовем такие строки статическими.


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


    Для использования библиотеки требуется как минимум C++14.


    Определение статической строки


    Определим статическую строку как массив символов, для удобства будем считать, что строка всегда оканчивается нулевым символом:


    template<size_t Size>
    using static_string = std::array<const char, Size>;
    
    constexpr static_string<6> hello = {'H', 'e', 'l', 'l', 'o', '\0'};

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


    Создание статической строки


    Посмотрите на определение строки hello выше, оно просто ужасно. Во-первых, нам нужно заранее вычислять длину массива. Во-вторых, нужно не забыть записать нулевой символ в конец. В-третьих, все эти запятые, скобки и кавычки. Определенно, с этим нужно что-то делать. Хотелось бы написать как-нибудь так:


    constexpr auto hello = make_static_string("hello");

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


    template<size_t Size, size_t ... Indexes>
    constexpr static_string<sizeof ... (Indexes) + 1> make_static_string(const char (& str)[Size]) {
        return {str[Indexes] ..., '\0'};
    }
    
    constexpr auto hello = make_static_string<0, 1, 2, 3, 4>("hello"); // hello == "hello"

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


    constexpr hello1 = make_static_string<1, 2, 3>("hello"); // hello1 == "ell"
    constexpr hello2 = make_static_string<4, 3, 2, 1, 0>("hello"); // hello2 == "olleh"

    Это соображение очень пригодится нам в дальнейшем.


    Теперь нам нужно как-то сгенерировать последовательность индексов строки. Для этого применим трюк с наследованием. Определим пустую структуру (нужно же что-то наследовать) с набором искомых индексов в качестве шаблонных параметров:


    template<size_t ... Indexes>
    struct index_sequence {};

    Определим структуру-генератор, которая будет генерировать индексы по одному, храня счетчик в первом параметре:


    template<size_t Size, size_t ... Indexes>
    struct make_index_sequence : make_index_sequence<Size - 1, Size - 1, Indexes ...> {};

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


    template<size_t ... Indexes>
    struct make_index_sequence<0, Indexes ...> : index_sequence<Indexes ...> {};

    В итоге, функция создания статической строки будет выглядеть так:


    template<size_t Size, size_t ... Indexes>
    constexpr static_string<sizeof ... (Indexes) + 1> make_static_string(const char (& str)[Size],
        index_sequence<Indexes ...>) {
            return {str[Indexes] ..., '\0'};
    }

    Напишем аналогичную функцию для статической строки, она пригодится нам далее:


    template<size_t Size, size_t ... Indexes>
    constexpr static_string<sizeof ... (Indexes) + 1> make_static_string(const static_string<Size>& str,
        index_sequence<Indexes ...>) {
            return {str[Indexes] ..., '\0'};
    }

    В дальнейшем, для каждой функции, принимающей строковый литерал foo(const char (& str)[Size]) будем писать аналогичную функцию, принимающую статическую строку foo(const static_string<Size>& str). Но я, для краткости, упоминать об этом не буду.


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


    template<size_t Size>
    constexpr static_string<Size> make_static_string(const char (& str)[Size]) {
        return make_static_string(str, make_index_sequence<Size - 1>{});
    }

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


    В случае отсутствия аргументов будем возвращать пустую статическую строку, которая состоит только из нулевого символа:


    constexpr static_string<1> make_static_string() {
        return {'\0'};
    }

    Также нам понадобится создавать строку из кортежа символов:


    template<char ... Chars>
    constexpr static_string<sizeof ... (Chars) + 1> make_static_string(char_sequence<Chars ...>) {
        return {Chars ..., '\0'};
    }

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


    Вывод статической строки в поток


    Здесь все просто. Так как наша строка оканчивается нулевым символом, достаточно вывести в поток данные массива:


    template<size_t Size>
    std::ostream& operator<<(std::ostream& os, const static_string<Size>& str) {
        os << str.data();
        return os;
    }

    Преобразование статической строки в std::string


    Здесь тоже ничего сложного. Инициализируем строку данными массива:


    template<size_t Size>
    std::string to_string(const static_string<Size>& str) {
        return std::string(str.data());
    }

    Сравнение статических строк


    Будем сравнивать строки посимвольно, пока не выявим различия, либо не достигнем конца хотя бы одной из строк. Поскольку constexpr for еще не изобрели, воспользуемся рекурсией и тернарным оператором:


    template<size_t Size1, size_t Size2>
    constexpr int static_string_compare(
        const static_string<Size1>& str1, 
        const static_string<Size2>& str2,
        int index = 0) {
            return index >= Size1 && index >= Size2 ? 0 :
                index >= Size1 ? -1 :
                    index >= Size2 ? 1 :
                        str1[index] > str2[index] ? 1 :
                            str1[index] < str2[index] ? -1 :
                                static_string_compare(str1, str2, index + 1);
    }

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


    template<size_t Size1, size_t Size2>
    constexpr int static_string_compare(
        const static_string<Size1>& str1, size_t index1,
        const static_string<Size2>& str2, size_t index2,
        size_t cur_length, size_t max_length) {
            return cur_length > max_length || (index1 >= Size1 && index2 >= Size2) ? 0 :
                index1 >= Size1 ? -1 :
                    index2 >= Size2 ? 1 :
                        str1[index1] > str2[index2] ? 1 :
                            str1[index1] < str2[index2] ? -1 :
                                static_string_compare(str1, index1 + 1, str2, index2 + 1, cur_length + 1, max_length);
    }

    Такая версия компаратора позволит нам сравнивать не только строки целиком, но и отдельные подстроки.


    Конкатенация статических строк


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


    template<size_t Size1, size_t ... Indexes1, size_t Size2, size_t ... Indexes2>
    constexpr static_string<Size1 + Size2 - 1> static_string_concat_2(
        const static_string<Size1>& str1, index_sequence<Indexes1 ...>,
        const static_string<Size2>& str2, index_sequence<Indexes2 ...>) {
        return {str1[Indexes1] ..., str2[Indexes2] ..., '\0'};
    }
    
    template<size_t Size1, size_t Size2>
    constexpr static_string<Size1 + Size2 - 1> static_string_concat_2(
        const static_string<Size1>& str1, const static_string<Size2>& str2) {
        return static_string_concat_2(str1, make_index_sequence<Size1 - 1>{},
            str2, make_index_sequence<Size2 - 1>{});
    }

    Реализуем также вариативный шаблон для конкатенации произвольного количества строк или строковых литералов:


    constexpr auto static_string_concat() {
        return make_static_string();
    }
    
    template<typename Arg, typename ... Args>
    constexpr auto static_string_concat(Arg&& arg, Args&& ... args) {
        return static_string_concat_2(make_static_string(std::forward<Arg>(arg)),
            static_string_concat(std::forward<Args>(args) ...));
    }

    Операции поиска в статической строке


    Рассмотрим операции поиска символа и подстроки в статической строке.


    Поиск символа в статической строке


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


    template<size_t Size>
    constexpr size_t static_string_find(const static_string<Size>& str, char ch, size_t from, size_t nth) {
        return Size < 2 || from >= Size - 1 ? static_string_npos :
            str[from] != ch ? static_string_find(str, ch, from + 1, nth) :
                nth > 0 ? static_string_find(str, ch, from + 1, nth - 1) : from;
    }

    Константа static_string_npos указывает на то, что поиск не увенчался успехом. Определим ее следующим образом:


    constexpr size_t static_string_npos = std::numeric_limits<size_t>::max();

    Аналогично реализуем поиск в обратном направлении:


    template<size_t Size>
    constexpr size_t static_string_rfind(const static_string<Size>& str, char ch, size_t from, size_t nth) {
        return Size < 2 || from > Size - 2 ? static_string_npos :
            str[from] != ch ? static_string_rfind(str, ch, from - 1, nth) :
                nth > 0 ? static_string_rfind(str, ch, from - 1, nth - 1) : from;
    }

    Определение вхождения символа в статическую строку


    Для определения вхождения символа достаточно попробовать поискать его:


    template<size_t Size>
    constexpr bool static_string_contains(const static_string<Size>& str, char ch) {
        return static_string_find(str, ch) != static_string_npos;
    }

    Подсчет количества вхождений символа в статическую строку


    Подсчет количества вхождений реализуется тривиально:


    template<size_t Size>
    constexpr size_t static_string_count(const static_string<Size>& str, char ch, size_t index) {
        return index >= Size - 1 ? 0 :
            (str[index] == ch ? 1 : 0) + static_string_count(str, ch, index + 1);
    }

    Поиск подстроки в статической строке


    Так как предполагается, что статические строки будут относительно небольшими, не будем здесь реализовывать алгоритм Кнута-Морриса-Пратта, реализуем простейший квадратичный алгоритм:


    template<size_t Size, size_t SubSize>
    constexpr size_t static_string_find(const static_string<Size>& str, const static_string<SubSize>& substr, size_t from, size_t nth) {
        return Size < SubSize || from > Size - SubSize ? static_string_npos :
            static_string_compare(str, from, substr, 0, 1, SubSize - 1) != 0 ? static_string_find(str, substr, from + 1, nth) :
                nth > 0 ? static_string_find(str, substr, from + 1, nth - 1) : from;
    }

    Аналогично реализуем поиск в обратном направлении:


    template<size_t Size, size_t SubSize>
    constexpr size_t static_string_rfind(const static_string<Size>& str, const static_string<SubSize>& substr, size_t from, size_t nth) {
        return Size < SubSize || from > Size - SubSize ? static_string_npos :
            static_string_compare(str, from, substr, 0, 1, SubSize - 1) != 0 ? static_string_rfind(str, substr, from - 1, nth) :
                nth > 0 ? static_string_rfind(str, substr, from - 1, nth - 1) : from;
    }

    Определение вхождения подстроки в статическую строку


    Для определения вхождения подстроки достаточно попробовать поискать ее:


    template<size_t Size, size_t SubSize>
    constexpr bool static_string_contains(const static_string<Size>& str, const static_string<SubSize>& substr) {
        return static_string_find(str, substr) != static_string_npos;
    }

    Определение, начинается/кончается ли статическая строка с/на заданной подстроки


    Применив ранее описанный компаратор мы можем определить, начинается ли статическая строка с заданной подстроки:


    template<size_t SubSize, size_t Size>
    constexpr bool static_string_starts_with(const static_string<Size>& str, const static_string<SubSize>& prefix) {
        return SubSize > Size ? false :
            static_string_compare(str, 0, prefix, 0, 1, SubSize - 1) == 0;
    }

    Аналогично для окончания статической строки:


    template<size_t SubSize, size_t Size>
    constexpr bool static_string_ends_with(const static_string<Size>& str, const static_string<SubSize>& suffix) {
        return SubSize > Size ? false :
            static_string_compare(str, Size - SubSize, suffix, 0, 1, SubSize - 1) == 0;
    }

    Работа с подстроками статической строки


    Здесь рассмотрим операции, связанные с подстроками статической строки.


    Получение подстроки, префикса и суффикса статической строки


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


    template<size_t Begin, size_t End, size_t ... Indexes>
    struct make_index_subsequence : make_index_subsequence<Begin, End - 1, End - 1, Indexes ...> {};
    
    template<size_t Pos, size_t ... Indexes>
    struct make_index_subsequence<Pos, Pos, Indexes ...> : index_sequence<Indexes ...> {};

    Реализуем получение подстроки с проверкой начала и конца подстроки с помощью static_assert:


    template<size_t Begin, size_t End, size_t Size>
    constexpr auto static_string_substring(const static_string<Size>& str) {
        static_assert(Begin <= End, "Begin is greater than End (Begin > End)");
        static_assert(End <= Size - 1, "End is greater than string length (End > Size - 1)");
        return make_static_string(str, make_index_subsequence<Begin, End>{});
    }

    Префикс — это подстрока, начало которой совпадает с началом исходной статической строки:


    template<size_t End, size_t Size>
    constexpr auto static_string_prefix(const static_string<Size>& str) {
        return static_string_substring<0, End>(str);
    }

    Аналогично для суффикса, только совпадает конец:


    template<size_t Begin, size_t Size>
    constexpr auto static_string_suffix(const static_string<Size>& str) {
        return static_string_substring<Begin, Size - 1>(str);
    }

    Разделение статической строки на две части по заданному индексу


    Чтобы разделить статическую строку по заданному индексу, достаточно вернуть префикс и суффикс:


    template<size_t Index, size_t Size>
    constexpr auto static_string_split(const static_string<Size>& str) {
        return std::make_pair(static_string_prefix<Index>(str), static_string_suffix<Index + 1>(str));
    }

    Реверсирование статической строки


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


    template<size_t Size, size_t ... Indexes>
    struct make_reverse_index_sequence : make_reverse_index_sequence<Size - 1, Indexes ..., Size - 1> {};
    
    template<size_t ... Indexes>
    struct make_reverse_index_sequence<0, Indexes ...> : index_sequence<Indexes ...> {};

    Теперь реализуем функцию, которая реверсирует статическую строку:


    template<size_t Size>
    constexpr auto static_string_reverse(const static_string<Size>& str) {
        return make_static_string(str, make_reverse_index_sequence<Size - 1>{});
    }

    Вычисление хэша статической строки


    Вычислять хэш будем по следующей формуле:


    H(s) = (s0 + 1) ⋅ 330 + (s1 + 1) ⋅ 331 +… + (sn — 1 + 1) ⋅ 33n — 1 + 5381 ⋅ 33n mod 264


    template<size_t Size>
    constexpr unsigned long long static_string_hash(const static_string<Size>& str, size_t index) {
        return index >= Size - 1 ? 5381ULL :
            static_string_hash(str, index + 1) * 33ULL + str[index] + 1;
    }

    Преобразование числа в статическую строку и обратно


    В этой главе рассмотрим преобразование статической строки в целое число, а также обратное преобразование. Для простоты будем считать, что числа представлены типами long long и unsigned long long, это типы большой разрядности, то есть подходят для большинства случаев.


    Преобразование числа в статическую строку


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


    Для получения всех цифр числа будем использовать генератор, аналогичный генератору последовательности индексов. Определим последовательность символов:


    template<char ... Chars>
    struct char_sequence {};    

    Реализуем генератор символов цифр, храня текущее число в первом параметре, а цифры в следующих, очередная цифра добавляется в начало последовательности, а число делится на десять:


    template<unsigned long long Value, char ... Chars>
    struct make_unsigned_int_char_sequence : make_unsigned_int_char_sequence<Value / 10, '0' + Value % 10, Chars ...> {};

    Если текущее число равно 0, то отбрасываем его, возвращая последовательность цифр, больше преобразовывать нечего:


    template<char ... Chars>
    struct make_unsigned_int_char_sequence<0, Chars ...> : char_sequence<Chars ...> {};

    Следует также учесть случай, когда первоначальное число равно нулю, в этом случае нужно вернуть нулевой символ, иначе нуль будет преобразован в пустую последовательность символов, а потом и в пустую строку:


    template<>
    struct make_unsigned_int_char_sequence<0> : char_sequence<'0'> {};

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


    template<bool Negative, long long Value, char ... Chars>
    struct make_signed_int_char_sequence {};

    Будем обрабатывать число также, как показано выше, но с учетом знака:


    template<long long Value, char ... Chars>
    struct make_signed_int_char_sequence<true, Value, Chars ...> :
        make_signed_int_char_sequence<true, Value / 10, '0' + -(Value % 10), Chars ...> {};
    
    template<long long Value, char ... Chars>
    struct make_signed_int_char_sequence<false, Value, Chars ...> :
        make_signed_int_char_sequence<false, Value / 10, '0' + Value % 10, Chars ...> {};

    Здесь есть один тонкий момент, обратите внимание на -(Value % 10). Здесь нельзя -Value % 10, так как диапазон отрицательных чисел на одно число шире диапазона положительных и модуль минимального числа выпадает из множества допустимых значений.


    Отбрасываем число после обработки, если оно отрицательно, добавим символ знака минуса:


    template<char ... Chars>
    struct make_signed_int_char_sequence<true, 0, Chars ...> : char_sequence<'-', Chars ...> {};
    
    template<char ... Chars>
    struct make_signed_int_char_sequence<false, 0, Chars ...> : char_sequence<Chars ...> {};

    Отдельно позаботимся о преобразовании нуля:


    template<>
    struct make_signed_int_char_sequence<false, 0> : char_sequence<'0'> {};

    Наконец, реализуем функции преобразования:


    template<unsigned long long Value>
    constexpr auto uint_to_static_string() {
        return make_static_string(make_unsigned_int_char_sequence<Value>{});
    }
    
    template<long long Value>
    constexpr auto int_to_static_string() {
        return make_static_string(make_signed_int_char_sequence<(Value < 0), Value>{});
    }

    Преобразование статической строки в число


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


    template<size_t Size>
    constexpr unsigned long long static_string_to_uint(const static_string<Size>& str, size_t index) {
        return Size < 2 || index >= Size - 1 ? 0 :
            (str[index] - '0') + 10ULL * static_string_to_uint(str, index - 1);
    }
    
    template<size_t Size>
    constexpr unsigned long long static_string_to_uint(const static_string<Size>& str) {
        return static_string_to_uint(str, Size - 2);
    }

    Для преобразования знаковых чисел, нужно учесть, что отрицательные числа начинаются с символа знака минуса:


    template<size_t Size>
    constexpr long long static_string_to_int(const static_string<Size>& str, size_t index, size_t first) {
        return index < first || index >= Size - 1 ? 0 :
            first == 0 ? (str[index] - '0') + 10LL * static_string_to_int(str, index - 1, first) :
                -(str[index] - '0') + 10LL * static_string_to_int(str, index - 1, first);
    }
    
    template<size_t Size>
    constexpr long long static_string_to_int(const static_string<Size>& str) {
        return Size < 2 ? 0 :
            str[0] == '-' ? static_string_to_int(str, Size - 2, 1) :
                static_string_to_int(str, Size - 2, 0); 
    }

    Вопросы удобства использования библиотеки


    К этому моменту библиотеку уже возможно полноценно использовать, но некоторые моменты вызывают неудобство. В этой главе рассмотрим как можно сделать использование библиотеки более удобным.


    Объект статической строки


    Упакуем строку и реализованные методы в объект. Это позволит использовать более короткие имена методов, а также реализовать операторы сравнения:


    template<size_t Size> struct static_string {
        constexpr size_t length() const {
            return Size - 1;
        }
        constexpr size_t size() const {
            return Size;
        }
        constexpr size_t begin() const {
            return 0;
        }
        constexpr size_t end() const {
            return Size - 1;
        }
        constexpr size_t rbegin() const {
            return Size - 2;
        }
        constexpr size_t rend() const {
            return std::numeric_limits<size_t>::max();
        }
        constexpr bool empty() const {
            return Size < 2;
        }
        constexpr auto reverse() const {
            return static_string_reverse(*this);
        }
        template<size_t Begin, size_t End> constexpr auto substring() const {
            return static_string_substring<Begin, End>(*this);
        }
        template<size_t End> constexpr auto prefix() const {
            return static_string_prefix<End>(*this);
        }
        template<size_t Begin> constexpr auto suffix() const {
            return static_string_suffix<Begin>(*this);
        }
        constexpr size_t find(char ch, size_t from = 0, size_t nth = 0) const {
            return static_string_find(*this, ch, from, nth);
        }
        template<size_t SubSize> constexpr size_t find(const static_string<SubSize>& substr, size_t from = 0, size_t nth = 0) const {
            return static_string_find(*this, substr, from, nth);
        }
        template<size_t SubSize> constexpr size_t find(const char (& substr)[SubSize], size_t from = 0, size_t nth = 0) const {
            return static_string_find(*this, substr, from, nth);
        }
        constexpr size_t rfind(char ch, size_t from = Size - 2, size_t nth = 0) const {
            return static_string_rfind(*this, ch, from, nth);
        }
        template<size_t SubSize> constexpr size_t rfind(const static_string<SubSize>& substr, size_t from = Size - SubSize, size_t nth = 0) const {
            return static_string_rfind(*this, substr, from, nth);
        }
        template<size_t SubSize> constexpr size_t rfind(const char (& substr)[SubSize], size_t from = Size - SubSize, size_t nth = 0) const {
            return static_string_rfind(*this, substr, from, nth);
        }
        constexpr bool contains(char ch) const {
            return static_string_contains(*this, ch);
        }
        template<size_t SubSize> constexpr bool contains(const static_string<SubSize>& substr) const {
            return static_string_contains(*this, substr);
        }
        template<size_t SubSize> constexpr bool contains(const char (& substr)[SubSize]) const {
            return static_string_contains(*this, substr);
        }
        template<size_t SubSize> constexpr bool starts_with(const static_string<SubSize>& prefix) const {
            return static_string_starts_with(*this, prefix);
        }
        template<size_t SubSize> constexpr bool starts_with(const char (& prefix)[SubSize]) const {
            return static_string_starts_with(*this, prefix);
        }
        template<size_t SubSize> constexpr bool ends_with(const static_string<SubSize>& suffix) const {
            return static_string_ends_with(*this, suffix);
        }
        template<size_t SubSize> constexpr bool ends_with(const char (& suffix)[SubSize]) const {
            return static_string_ends_with(*this, suffix);
        }
        constexpr size_t count(char ch) const {
            return static_string_count(*this, ch);
        }
        template<size_t Index> constexpr auto split() const {
            return static_string_split<Index>(*this);
        }
        constexpr unsigned long long hash() const {
            return static_string_hash(*this);
        }
        constexpr char operator[](size_t index) const {
            return data[index];
        }
        std::string str() const {
            return to_string(*this);
        }
        std::array<const char, Size> data;
    };

    Операторы сравнения


    Использование компаратора в виде функции неудобно и нечитаемо. Определим глобальные операторы сравнения:


    template<size_t Size1, size_t Size2>
    constexpr bool operator<(const static_string<Size1>& str1, const static_string<Size2>& str2) {
        return static_string_compare(str1, str2) < 0;
    }

    Аналогично реализуем остальные операторы > <= >= == !=, для всех вариаций аргументов статических строк и строковых литералов. Здесь приводить их нет смысла из-за тривиальности.


    Макросы работы с числами


    Для удобства преобразования числа в статическую строку и обратно определим соотвествующие макросы:


    #define ITOSS(x) int_to_static_string<(x)>()
    #define UTOSS(x) uint_to_static_string<(x)>()
    #define SSTOI(x) static_string_to_int((x))
    #define SSTOU(x) static_string_to_uint((x))

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


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


    Конкатенация статических строк и строковых литералов:


    constexpr auto hello = make_static_string("Hello");
    constexpr auto world = make_static_string("World");
    constexpr auto greeting = hello + ", " + world + "!"; // greeting == "Hello, World!"

    Конкатенация статических строк, строковых литералов и чисел:


    constexpr int apples = 5;
    constexpr int oranges = 7;
    constexpr auto message = static_string_concat("I have ", ITOSS(apples), 
        " apples and ", ITOSS(oranges), ", so I have ", ITOSS(apples + oranges), " fruits");
    // message = "I have 5 apples and 7 oranges, so I have 12 fruits"    

    constexpr unsigned long long width = 123456789ULL;
    constexpr unsigned long long height = 987654321ULL;
    constexpr auto message = static_string_concat("A rectangle with width ", UTOSS(width), 
        " and height ", UTOSS(height), " has area ", UTOSS(width * height));
    // message = "A rectangle with width 123456789 and height 987654321 has area 121932631112635269"    

    constexpr long long revenue = 1'000'000LL;
    constexpr long long costs = 1'200'000LL;
    constexpr long long profit = revenue - costs;
    constexpr auto message = static_string_concat("The first quarter has ended with net ",
        (profit >= 0 ? "profit" : "loss  "), " of $", ITOSS(profit < 0 ? -profit : profit));
    // message == "The first quarter has ended with net loss   of $200000"

    Парсинг URL:


    constexpr auto url = make_static_string("http://www.server.com:8080");
    constexpr auto p = url.find("://");
    constexpr auto protocol = url.prefix<p>(); // protocol == "http"
    constexpr auto sockaddr = url.suffix<p + 3>();
    constexpr auto hp = sockaddr.split<sockaddr.find(':')>();
    constexpr auto host = hp.first; // host == "www.server.com"
    constexpr int port = SSTOI(hp.second); // port == 8080

    Итерация по символам в обоих направлениях:


    constexpr auto str = make_static_string("Hello");
    for (size_t i = str.begin(); i != str.end(); ++i) // вперед
        std::cout << str[i];
    std::cout << std::endl; // Hello
    for (size_t i = str.rbegin(); i != str.rend(); --i) // назад
        std::cout << str[i];
    std::cout << std::endl; // olleH

    Ссылки


    Библиотеку, реализующую все вышеперечисленное, можно взять в моем github


    Спасибо за внимание, замечания и дополнения приветствуются.


    Update


    Реализовал пользовательский литерал _ss для создания статических строк из строковых литералов:


    template<typename Char, Char ... Chars>
    constexpr basic_static_string<Char, sizeof ... (Chars) + 1> operator"" _ss() {
        return {Chars ..., static_cast<Char>('\0')};
    };

    Функцию make_static_string() запрятал во внутренний немспейс, все стало выглядеть приятнее:


    constexpr auto hello_world = "Hello"_ss + " World";
    if ("Hello" < "World"_ss) { ... }
    constexpr auto hash = "VeryLongString"_ss.hash();

    Добавил шаблонный параметр Char вместо char:


    template<typename Char, size_t Size> struct basic_static_string {
        // ...
        std::array<const Char, Size> data;
    };

    Сделал специализации для char и whar_t, нижние используются в качестве неймспейсов, чтобы дергать статическую concat, которую внес в структуру статической строки:


    template<size_t Size> using static_string_t = basic_static_string<char, Size>;
    template<size_t Size> using static_wstring_t = basic_static_string<wchar_t, Size>;
    
    using static_string = basic_static_string<char, 0>;
    using static_wstring = basic_static_string<wchar_t, 0>;

    Теперь все работает и для "широких" литералов:


    constexpr auto wide_string = L"WideString"_ss;
    
    constexpr int apples = 5;
    constexpr int oranges = 7;
    constexpr int fruits = apples + oranges;
    constexpr auto str3 = static_wstring::concat(L"I have ", ITOSW(apples), L" apples and ",
        ITOSW(oranges), L" oranges, so I have ", ITOSW(fruits), L" fruits");
    static_assert(str3 == L"I have 5 apples and 7 oranges, so I have 12 fruits", "");
    std::wcout << str3 << std::endl;

    Поправил метод size(), теперь size() и length() возвращают длину строки без учета нулевого символа, для получения размера массива нужно использовать sizeof():


    constexpr auto ss1 = "Hello"_ss;
    static_assert(ss1.length() == 5, "");
    static_assert(ss1.size() == 5, "");
    static_assert(sizeof(ss1) == 6, "");

    Обновленная версия лежит на github
    Спасибо всем за полезные комментарии.


    Update 2


    В ходе обсуждения с AndreySu появился еще один способ реализации статических строк, где символы передаются как шаблонные параметры:


    #include <iostream>
    
    using namespace std;
    
    template<typename Char, Char ... Chars> struct static_string{};
    
    template<typename Char, Char ... Chars1, Char ... Chars2>
    constexpr static_string<Char, Chars1 ..., Chars2 ... > operator+(
        const static_string<Char, Chars1 ... >& str1,
        const static_string<Char, Chars2 ... >& str2) {
        return static_string<Char, Chars1 ..., Chars2 ...>{};
    }
    
    template<typename Char, Char ch, Char ... Chars>
    std::basic_ostream<Char>& operator<<(std::basic_ostream<Char>& bos, const static_string<Char, ch, Chars ...>& str) {
        bos << ch << static_string<Char, Chars ... >{};
        return bos;
    }
    
    template<typename Char>
    std::basic_ostream<Char>& operator<<(std::basic_ostream<Char>& bos, const static_string<Char>& str) {
        return bos;
    }
    
    template<typename Char, Char ... Chars>
    constexpr static_string<Char, Chars ... > operator"" _ss() {
        return static_string<Char, Chars ... >{};
    };
    
    int main() {
        constexpr auto str1 = "abc"_ss;
        constexpr auto str2 = "def"_ss;
        constexpr auto str = str1 + str2 + str1;
        std::cout << str << std::endl;
    
        return 0;
    }

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 47

      +7

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

        0

        Спасибо, учту

        +1

        А почему написали свой велосипед, а не использовали, например, boost::hana?

          0

          Ну, хотя бы в образовательных целях
          Я конечно же подозревал, что кто-то уже сделал нечто подобное

          0
          Возможно ли такие строки определять в шаблонных параметрах?
            0

            Нет, но можно например использовать такой workaround с помощью хэшей:


            template<unsigned long long> constexpr auto greet() {return "Hello, guest!";}
            template<> constexpr auto greet<static_string_hash("alice")>() {return "Hello, Alice!";}
            template<> constexpr auto greet<static_string_hash("bob")>() {return "Hello, Bob!";}
            
            constexpr auto name = make_static_string("alice");
            constexpr auto greeting = greet<name.hash()>();
            std::cout << greeting << std::endl;
              0
              Не очень понял где тут хэш и как достать саму строку из щаблонного параметра?
                0

                Прошу прощения, прочитал "определять" как "передавать"
                В C++ известная проблема со строковыми литералами в шаблонных параметрах, поэтому если так хочется именно в шаблонных параметрах, то придется определять строку как список символов, например так:


                template<char ... Chars> struct static_string{};
                
                template<char ... Chars1, char ... Chars2>
                constexpr auto static_string_concat(
                const static_string<Chars1 ... >& str1,
                const static_string<Chars2 ... >& str2) {
                    return static_string<Chars1 ..., Chars2 ...>{};
                }
                
                template<char ch, char ... Chars>
                std::ostream& operator<<(std::ostream& os, const static_string<ch, Chars ...>& str) {
                    os << ch << static_string<Chars ... >{};
                    return os;
                }
                
                std::ostream& operator<<(std::ostream& os, const static_string<>& str) {
                    return os;
                }
                
                constexpr static_string<'a', 'b', 'c'> str1;
                constexpr static_string<'d', 'e', 'f'> str2;
                constexpr auto str = static_string_concat(str1, str2);
                std::cout << str << std::endl;

                Честно говоря, этот вариант я тоже рассматривал, но в итоге ничего толкового из него не вышло

                  0
                  Да, этот вариант известен и можно сделать через препроцессор, но при этом препроцессор сможет создать только фиксированную длину строки в шаблоне, и забить нулями окончание, если символов меньше чем эта фиксированная длина.
                    0

                    Все-таки возможно сделать этот вариант хорошо с помощью оператора пользовательского литерала:


                    template<typename Char, Char ... Chars>
                    constexpr static_string<Char, Chars ... > operator"" _ss() {
                        return static_string<Char, Chars ... >{};
                    };
                    
                    constexpr auto str1 = "abc"_ss;
                    constexpr auto str2 = "def"_ss;
                    constexpr auto str = str1 + str2 + str1;
                    std::cout << str << std::endl;

                    Пожалуй, вынесу это в статью

                      0
                      Пользовательские литералы и без шаблона могут взять строку:
                      constexpr MyType operator "" _ss(const char* str, const size_t size)
                      {
                      // ...
                      }
                      
            +2
            Вместо имен вида static_string_concat было бы наверное красивее создать отдельный неймспейс в стиле static_string::concat
            Тип символов вынести в шаблонный параметр для поддержки wchat_t
              –12
              За это мне больше нравится Delphi.
                +2
                Нехорошо, что Ваш тип struct static_string, с интерфейсом, похожим на std::string, в отличие от последнего имеет функции
                    constexpr size_t length() const {
                        return Size - 1;
                    }
                    constexpr size_t size() const {
                        return Size;
                    }
                
                , возвращающие разные значения, что может приводить к множеству ошибок.
                  0

                  Согласен, заменю на sizeof

                  –6
                  В то время как rust научился проверять валидность sql запросов на этапе компиляции, в цпп не могут проинициализировать и сложить 2 строчки без UB и рулонов кода.
                    +2
                    ну… покажите тогда НЕ рулоны кода, где руст «научился» парсить запрос
                      +2
                      А Rust уже научился автоматически конвертировать существующий C++ный код в код на Rust-е? Ну вот чтобы не нужно было сопровождать мегатонны унаследованного C++ного кода, который, сюрпраз-сюрпрайз, работает и приносит деньги. И чтобы не нужно было этот код вручную с нуля на Rust переписывать. А просто запустил волшебный тул и получил вместо копролитов мамонта на C++ свежий и благоухающий фиалками код на Rust-е.
                        0
                        подозреваю, что это невозможно. Возможна только обратная конвертация.
                        +1

                        Давайте ссылку на вашу проверку кода в языке.

                          0
                          У меня есть самописная библиотечка, генерирующая запросы по типам данных (и, естественно, написанным на плюсах выражениям, для where/etc). Джойны, поддержка агрегатных функций, проверки, что не сравниваете апельсины с бананами или что не делаете джойны в where-условиях delete (а можно было бы сделать с субзапросом, но мне пока лень). Масштаба тыщи строк где-то.
                            0
                            А чем джойны в where-условии delete отличаются от джойнов в where-условии select или update?
                              0
                              Тем, что их там нельзя, по крайней мере, в sqlite.

                              Посмотрел исходники, чего там как у меня — похоже, я с update перепутал. Насчёт delete не знаю, смотреть надо.
                          +3
                          Поскольку constexpr for еще не изобрели

                          Э… https://wandbox.org/permlink/o1dC3wWbXzEPy7Z1
                          Вы точно про C++14 говорите?
                            0

                            Голову пеплом посыпал, спасибо

                              0
                              Это с++17, а у автора с++14.
                              –8
                              Ужас! Лучше Qt использовать! :)
                                –1
                                Почему нельзя решить проблему с помощью препроцессора? Некрасиво???
                                  +2
                                  Статья очень напоминает вот эту akrzemi1.wordpress.com/2017/06/28/compile-time-string-concatenation. Может вы забыли тег «перевод»?
                                    0

                                    Нет, не забыл. Действительно очень похоже.

                                    0
                                    >порядок инициализации глобальных объектов не определен

                                    Это почему это? Можно заставить компилятор инициализировать глобальные переменные в определенном порядке.
                                    Для GCC это init_priority
                                    Для VC++ #pragma init_seg
                                      0

                                      В стандарте не определен насколько я помню, init_priority и #pragma init_seg это фишки отдельных компиляторов

                                      • UFO just landed and posted this here
                                      0
                                      а для чего это надо то
                                      auto str = «hello» + «world»;
                                      если можно сразу написать
                                      auto str = «helloworld»;
                                      т.е. просто в какой момент получается что всё статично, но при этом не совсем? как?
                                        0

                                        В статье есть пример с плагинами, есть директория с плагинами path = "/path/to/plugins/" и два плагина plugin1 = "plugin1.so", plugin2 = "plugin2". Чтобы не писать два раза путь так plugin1_path = "/path/to/plugins/plugin1" и plugin2_path = "/path/to/plugins/plugin2" мы используем конкатенацию plugin1_path = path + "plugin1" и plugin2_path = path + "plugin2"
                                        Да, все полностью статично, то есть конкатенация происходит на этапе компиляции

                                          0
                                          #define path "/path/to/plugins/"
                                          #define plugin1_path path «plugin1»
                                          #define plugin2_path path «plugin2»
                                          Не?
                                            0

                                            Как вариант, правда я не уверен, что все операции со строками, которые описаны в статье, возможно реализовать на макросах

                                              0
                                              Мня опередили по макросам )
                                              Путь до плагина сколько раз в программе используется? Один раз при запуске? Это экономия на спичках, мне кажется, и приведет она не только к захламлению кода но и к чему то типа «стоимость товара: один рубля»…

                                              P.S. Была книга 10 лет назад по шаблонам где человек считает таким методом чуть ли не интегралы, а результат программы лишь вывод окончательного результата. Но тема так и не пошла )) И кстати, Intellisense какбы не повесился.
                                                0

                                                Если у Вас в проекте это не дает большого выигрыша, то конечно, не следует это использовать. Я уверен, в большинстве случаев constexpr char[] будет достаточно.
                                                P.S. А в Nvidia, например, используют compile-time хэш таблицу
                                                https://www.youtube.com/watch?v=kUbWYdlS9v0&list=PLZN9ZGiWZoZoFa2q0NqD6metQxavT2JYP&index=19

                                                  0
                                                  Да я не говорю что это плохо, спасибо, на самом деле очень интересно было посмотреть на результат. Уверяю Вас, сейчас компьютеры очень не тупые и «молотят» так, что иногда удивляешься. Я тут недавно делал проект OpenGL с 2д физикой сурьезной, 300-400тел, и сам не понимал в начале реально ли, потянет ли современный комп. Короче, в результате получил 500-600фпс вместе с отрисовкой, и понял какая это мощь… Видели бы вы эти расчеты… После этого конкатенация строк выглядит как шутка
                                            +1
                                            const char* helloWorld = "Hello "
                                            "world"

                                            Не?.. Конкатенация на этапе компияции.
                                              0

                                              char потом нужно будет преобразовать в string в runtime. А воще смешивать string и char это потенциальный выстрел в ногу.

                                                0

                                                И тут на помощь приходит string_view. Только вместо const char* надо auto для начала

                                          0

                                          Обновил статью с учетом замечаний

                                            0

                                            Ну, тогда докину еще одно ;)
                                            КМК путь к директории может быть записан и без разделителя в конце. Если кто-то не сильно внимательный исправит PLUGIN_PATH вот так:


                                            const std::string PLUGIN_PATH = "/usr/local/lib/project/plugins";

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

                                            0

                                            Добавил альтернативный вариант реализации статических строк

                                            Only users with full accounts can post comments. Log in, please.