Как избежать дублирования текста строкового литерала в функциях, которые принимают символы/строки разной ширины?

Мне часто приходится создавать функции манипулирования строками на 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* в качестве значений в параметрах шаблона.

Мне не хватает некоторых требований: должны ли входные строки поддерживать кодировку/многобайтовые символы, или строковые литералы в основном будут символами в диапазоне ASCII?

Pepijn Kramer 17.05.2024 22:35

Я думал в этом направлении: onlinegdb.com/TWWUMHJ25

Pepijn Kramer 17.05.2024 23:01

@PepijnKramer, который не поддерживает MBCS. В частности, все варианты UTF являются MBCS. Преобразование кодировки — это уже головная боль. Сделать это constexpr — это просто положить соль на кровоточащую рану.

Red.Wave 18.05.2024 09:28

@Red.Wave Я полностью это осознаю, поэтому следует добавить функции преобразования перегрузки. А также, почему я не опубликовал это как ответ (это направление без МАКРО)

Pepijn Kramer 18.05.2024 10:02

@PepijnKramer Мой пример кода заключался в одной замене.

Red.Wave 18.05.2024 12:20
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
175
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Мы можем создать строковый литерал, который будет хранить как узкие, так и широкие строки.

Хитрость заключается в том, чтобы использовать трюк со строковыми литералами во время компиляции, но заполнять как буфер 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++.

J.S. Hughes 18.05.2024 13:20

Кроме того, судя по комментариям в исходном вопросе, создается наивная копия, которую можно использовать только в диапазоне ascii. Этого достаточно для того, что я обычно делаю, но для утилиты общего назначения это необходимо учитывать. Может быть, где-то static_assert(c < 128).

J.S. Hughes 18.05.2024 13:49

Другие вопросы по теме