Если я правильно понимаю, то в C и C++ строковые литералы хранятся в статической памяти. Поэтому указатель на строковый литерал остается валидным, фактически, до самого конца работы программы. Т.е. если мы пишем что-то вроде:
class long_living_object { const char * value_; public: long_living_object(const char * v) : value_{v} {} ... }; long_living_object make_object_ok() { return { "Hello, World!" }; } |
то у нас, в принципе, все нормально. В возвращаемом функцией make_object_ok объекте будет находиться валидный указатель.
К сожалению, в C++ нет никакого способа отличить указатель на строковый литерал от указателя на какой-то временный массив символов. Т.е., ничто в языке не запрещает нам сделать так:
long_living_object make_object_bug() { char greeting[] = "Hello, World!"; return { greeting }; } |
Чтобы использовать объекты long_living_object было безопасно, нужно скопировать содержимое передаваемой строки вовнутрь. Что требует дополнительных ресурсов, особенно когда длина строки нам заранее неизвестна и мы вынуждены использовать динамическую память.
Все это особенно обидно в ситуациях, когда мы точно знаем, что long_living_object должен содержать указатели только на строковые литералы. Например, когда long_living_object -- это что-то вроде:
struct negative_response { std::uint16_t status_code_; const char * reason_phrase_; }; |
и negative_response::reason_phrase_ содержит указатели на строковые литералы с заранее заготовленными описаниями стандартных статус-кодов HTTP.
Мы можем использовать тип negative_response в коде, сопровождая этот код предупреждениями, что reason_phrase_ должен быть указателем на строковый литерал. И какое-то (возможно даже долгое) время это будет работать. Пока кто-нибудь, где-нибудь, когда-нибудь не ошибется. И компилятор нам этого никак не поможет предотвратить такую ошибку.
И в этом плане я очень завидую Rust-у, где есть специальный lifetime обозначаемый как 'static. Если вы в Rust-е получаете ссылку с таким lifetime, то можете быть уверены, что эта ссылка не "повиснет". Но, самое важное, Rust вам не позволит подсунуть ссылку на временный объект туда, где ожидается 'static.
И до недавнего времени я думал, что в C++ с указателями на строковые литералы нам остается только облизываться на то, что есть в Rust-е.
Конечно же, были способы представления строк, которыми можно оперировать в compile-time. Вот здесь можно найти вариант, как раз подходящий для решения обсуждаемой проблемы.
Но эти варианты мне не нравились тем, что у каждой такой строки получался свой собственный тип. И нужно было поломать голову над тем, как заставить работать типы, вроде показанных выше long_living_object/negative_response, с compile-time строками на базе шаблонов, параметризованных последовательностями символов.
Давеча в голову пришла мысль, что можно использовать user-defined literals из C++11 и более свежих C++ных стандартов. Поскольку определенный пользователем operator для преобразования строкового литерала в пользовательский тип применяется именно к строковому литералу (а литералы живут в статической памяти), то указателю на этот литерал мы можем доверять.
Значит, остается сделать такой тип-обертку вокруг указателя на char, экземпляры которого мог бы создавать только operator для user-defined literals. Что делается элементарно посредством приватного конструктора и объявления friend-а для этого типа.
В результате получается вот такое тривиальное решение (я не заморачивался здесь на расстановку constexpr, хотя практически все в коде может и должно быть отмечено как constexpr):
namespace utils { class string_literal; namespace literals { [[nodiscard]] string_literal operator""_str(const char * value, std::size_t) noexcept; } /* namespace literals */ class string_literal { const char * m_value; string_literal(const char * value) noexcept : m_value{value} {} public: [[nodiscard]] const char * raw_value() const noexcept { return m_value; } friend string_literal literals::operator""_str(const char * value, std::size_t) noexcept; }; namespace literals { [[nodiscard]] inline string_literal operator""_str(const char * value, std::size_t) noexcept { return { value }; } } /* namespace literals */ } /* namespace utils */ |
Используется это следующим образом:
int main() { using namespace utils::literals; const auto v1 = "first"_str; auto v2 = "second"_str; auto v3 = v1; std::cout << v1.raw_value() << ", " << v2.raw_value() << ", " << v3.raw_value() << std::endl; } |
Поиграться с реализацией можно здесь: https://wandbox.org/permlink/QAhaCIzhF8xj3mVT.
Имея такой класс string_literal мы можем написать:
struct negative_response { std::uint16_t status_code_; utils::string_literal reason_phrase_; }; |
И при этом сам компилятор будет бить нам по рукам за попытки засунуть в negative_response::reason_phrase_ указатель на временный массив char-ов. Но, при этом, все строковые литералы будут представляться одним и тем же типом, вне зависимости от содержимого литерала, что дает нам возможность делать так:
using namespace utils::literals; static const auto not_found = "Not Found"_str; static const auto bad_request = "Bad Request"_str; static const auto access_denied = "Access Denied"_str; ... std::variant<ok, negative_response> check_access(const request_data & rd) { ... if(!has_permission(rd)) return negative_response{403, access_denied}; ... } |
Нужно пояснить почему в приведенной реализации string_literal есть вспомогательное пространство имен literals.
Дело в том, что, насколько я помню, нельзя вызывать user-defined literals operator с указанием пространства имен из которого этот оператор нужно вызывать. Т.е. нельзя написать так:
const auto v1 = "first"utils::literals::_str; |
Значит, перед тем, как использовать operator""_str нужно сделать using namespace. Что не очень хорошо, если нам нужно использовать operator""_str где-то в заголовочном файле.
Поэтому, имхо, лучше разместить operator""_str в отдельном крошечном пространстве имен, применение using namespace для которого не будет проблемой, даже если это выполняется в заголовочных файлах.
Решил поделиться этой идеей с читателями блога.
Мне кажется, что подобный string_literal вполне легален и никаких проблем при его использовании быть не должно.
Но, т.к. я не очень хорошо понимаю, что и в какие сегменты памяти распределяется компилятором/линкером, то могу и ошибаться. Если кто-то видит в предложенном решении фатальный недостаток, то не сочтите за труд, дайте, пожалуйста, знать.
Да, на оригинальность идеи ни в коем случае не претендую. Не помню, чтобы мне доводилось читать о таком подходе именно в этом виде. Но, вполне возможно, что когда-то что-то на глаза попадалось, а вспомнилось вот только на днях.
Естественно, ни о какой безопасности использования string_literal не может быть и речи, если мы начинаем вручную загружать и выгружать динамические библиотеки (dll/so). Если литерал определен внутри вручную загруженной DLL, а потом мы эту DLL вручную выгрузили, то это неизбежно приведет к появлению повисших указателей.
5 комментариев:
Да, делал такой, назвал его _lit :)
Главное использование - для передачи строк из рабочего потока в логирующий (чтоб не тормозить рабочий).
Я начал использовать `static const std::string_view` для строковых констант, но не додумался до собственного оператора. Спасибо за идею, попробую использовать `struct string_literal : string_view {}` с закрытом конструктором.
@Pavel
Я тоже сперва использовал static const std::string_view, но здесь такая же засада, как и с просто const char*. Поэтому все равно продолжал думать в сторону аналога 'static из Rust-а.
> попробую использовать `struct string_literal : string_view {}` с закрытом конструктором.
Интересная мысль. Я до такого радикального решения пока не дошел, ограничился вот этим: https://github.com/Stiffstream/arataga/blob/4d93aadf86a5914b021d622b693069437dceba54/arataga/utils/string_literal.hpp#L106-L133
@eao197
У вас в string_literal есть оператор конверсии в std::string_view, так что при желании можно использовать все его функции-члены. Мой вариант с публичным наследованием - это больше от лени :)
@Pavel
Просто в моих сценариях с содержимым литерала вообще ничего не нужно было делать кроме записи в лог и/или сокет. А вот если бы нужно было литералы сравнивать, вычленять фрагменты, искать подстроки и т.д., то тут нужно было бы думать. И тогда вариант с наследованием от string_view следовало бы рассмотреть самым тщательным образом.
Хотя, наверное, в этом случае какой-нибудь substr должен был бы возвращать string_literal, а не string_view.
Отправить комментарий