Мне часто приходится создавать функции манипулирования строками на C++. Мои API, как правило, написаны так, чтобы принимать std::basic_string<T> (эффективно), но я также хочу принимать std::basic_string_view<T>, когда это имеет смысл. При принятии string_view было несколько проблем, над которыми я работал, но с литералами я обнаружил только боль.
Теперь, будучи хорошим разработчиком, я ненавижу как встроенные литералы, так и дублирование кода.
Я обнаружил, что пытаюсь написать это:
template <typename CharType>
constexpr const CharType* HELLO_WORLD = "Hello World!";
Это не работает, особенно потому, что если CharType нет char, мне нужен u или U или L.
Мне бы хотелось решение, которое выглядит так:
template <typename CharType>
static const CharType* HELLO_WORLD = <CharType>"Hello World!";
Моя текущая альтернатива требует макросов, которые создают структуры, специализирующиеся на макросах:
#define DEFINE_STRING_LITERAL(name, str) \
template <typename CharType> \
struct name {}; \
\
template <> \
struct name<char> { \
static constexpr const char* value = str; \
}; \
\
template <> \
struct name<wchar_t> { \
static constexpr const wchar_t* value = L##str; \
}; \
\
template <> \
struct name<char16_t> { \
static constexpr const char16_t* value = u##str; \
}; \
\
template <> \
struct name<char32_t> { \
static constexpr const char32_t* value = U##str; \
};
Что определяется следующим образом:
DEFINE_STRING_LITERAL(HELLO_WORLD ,"value");
и используется следующим образом:
HELLO_WORLD<CharType>::value
Это нормально, я могу запихнуть макрос куда-нибудь в заголовок и забыть о нем. Но мне не нравится синтаксис определения. Это не похоже на определение, а затем пытаться найти, где оно определено, с помощью статических инструментов, если мне нужно его изменить, — это головная боль. Кроме того, он не выводится по типу, что затрудняет чтение полученного кода.
Компилятор знает, какие литеральные префиксы с какими типами связаны. Я знаю это, потому что он не скомпилируется, если я его испорчу. Итак, есть ли способ избежать этой специализации структур и получить что-то ближе к однострочнику, который я хотел бы видеть?
Если у кого-то есть лучшее решение, которое больше похоже на стандарт C++, а не на использование макроса для определения типа на лету, это было бы здорово.
P.S. Вероятно, я мог бы создать константный класс со своим собственным синтаксисом и хранить char*, но тогда мне нужно будет копировать строки, а не позволять компилятору управлять ими. Мне бы также хотелось, чтобы это были действительно статические литералы, поскольку я не могу использовать динамические const CharType* в качестве значений в параметрах шаблона.
Я думал в этом направлении: onlinegdb.com/TWWUMHJ25
@PepijnKramer, который не поддерживает MBCS. В частности, все варианты UTF являются MBCS. Преобразование кодировки — это уже головная боль. Сделать это constexpr — это просто положить соль на кровоточащую рану.
@Red.Wave Я полностью это осознаю, поэтому следует добавить функции преобразования перегрузки. А также, почему я не опубликовал это как ответ (это направление без МАКРО)
@PepijnKramer Мой пример кода заключался в одной замене.





Мы можем создать строковый литерал, который будет хранить как узкие, так и широкие строки.
Хитрость заключается в том, чтобы использовать трюк со строковыми литералами во время компиляции, но заполнять как буфер char, так и wchar_t. Затем поддержите указатели на оба.
Затем сделайте так, чтобы эти литералы времени компиляции существовали в типе как аргументы шаблона, не относящиеся к типу. Сохраните их статическую копию внутри класса и возвращайте указатели на любой необходимый буфер.
Вы собираетесь писать свой код, используя как узкие, так и широкие строки — тип будет общим, поэтому экземпляр будет создаваться только один раз.
То есть что-то вроде:
static_str< "abc 123"_both >{}
создает класс static_str, который можно неявно преобразовать во что угодно, использующее либо char const*, либо wchar_t const*. Его состояние живет в его типе, поэтому две копии static_str< "abc 123"_both >{} совместно используют базовые буферы (экземпляры не имеют состояния).
Я предполагаю, что это несколько десятков строк кода, некоторые из которых сложны и используют относительно новые стандартные версии.
Во-первых, изучите строки времени компиляции; заставить синтаксис "hello"_ct работать. Вы обнаружите, что хранить буферы char и wchar_t очень просто. (ключевые слова здесь — «определяемые пользователем литералы» для синтаксиса _ct и строки времени компиляции; вы можете получить длину буфера во время компиляции и передать ее в свой класс управления строковым буфером во время компиляции в качестве параметра шаблона, не являющегося типом (так что он может иметь размер массива фиксированного размера), затем constexpr копирует символы.)
Затем static_string<auto ct_string>, где он сохраняет ct_string как статическую переменную и поддерживает приведение к char const* и wchar_t const* тогда и только тогда, когда ct_string это делает.
Для простоты использования вам может потребоваться поддержка большего, чем просто приведение к char-указателю - приведение к чему-либо, что можно построить из char-указателя, является более щедрой версией. OTOH, простой вызов .get<CHAR_TYPE>(), который возвращает версию CHAR_TYPE const*, — это менее бестолковое волшебство.
Я уезжаю на выходные, но думаю, что написать это будет весело. Просто надо идти. Если у вас возникнут проблемы, напишите мне, и я посмотрю, что можно написать.
Сначала определите шаблон строкового класса времени компиляции, который инициализирует массив каждого типа символов:
template <std::size_t N>
class ct {
std::tuple<char[N], wchar_t[N], char8_t[N], char16_t[N], char32_t[N]> arrays;
public:
consteval ct(const char (&literal)[N]) {
std::apply(
[&](auto &...array) { (..., std::ranges::copy(literal, array)); },
arrays);
}
...
};
Затем определите метод, который возвращает представление данного типа символов:
template <class Char>
consteval std::basic_string_view<Char> view() const {
return {std::get<Char[N]>(arrays), N - 1};
}
Чтобы использовать его, просто определите константу:
inline constexpr ct test = "Jon Skeet";
static_assert(test.view<char>() == "Jon Skeet"sv);
static_assert(test.view<wchar_t>() == L"Jon Skeet"sv);
static_assert(test.view<char8_t>() == u8"Jon Skeet"sv);
static_assert(test.view<char16_t>() == u"Jon Skeet"sv);
static_assert(test.view<char32_t>() == U"Jon Skeet"sv);
Попробуйте в Compiler Explorer
Спасибо. Я хочу попытаться избежать хранения нескольких копий данных. С этим нет проблем с функциональным решением моих проблем, но в конечном итоге мне нужны те же гарантии, что и для шаблона constexpr, т.е. что я получаю байты в своем объектном коде только для типов, экземпляры которых создаются. Я пока не уверен, что это возможно без новых возможностей C++.
Кроме того, судя по комментариям в исходном вопросе, создается наивная копия, которую можно использовать только в диапазоне ascii. Этого достаточно для того, что я обычно делаю, но для утилиты общего назначения это необходимо учитывать. Может быть, где-то static_assert(c < 128).
Мне не хватает некоторых требований: должны ли входные строки поддерживать кодировку/многобайтовые символы, или строковые литералы в основном будут символами в диапазоне ASCII?