Страницы

Поиск по вопросам

среда, 29 января 2020 г.

std::string vs const std::string& vs std::string_view

#cpp


Что правильнее передавать как аргумент функции(например в конструктор), если там
эта строка будет просто скопирована?
    


Ответы

Ответ 1



Использование любого из альтернативных средств очень сильно зависит от ситуации, и выбирается исходя из конкретных условий. И это относится не только к std::string и std::string_view Для начала определимся с реализацией наших сущностей. Пусть std::string построен на трех указателях по 4 байта каждый (или указатель и два размера): class string { //... pointer * m_begin; //Начало строки pointer * m_end; //Указатель за последний элемент pointer * m_end_of_storage; //Указатель за конец выделенной памяти }; std::string_view при этом реализован с помощью двух указателей (или указатель и размер): class string_view { //... pointer m_data; //Указатель на начало данных pointer m_end; //Указатель за конец данных }; Упрощенно рассмотрим три версии простого кода: void other(std::string); //<-- функция, в которую всегда передается копия. void run(string_view v) //<-- Функция, которая вызывает функцию other { //т.е. эта функция делает ту самую копию объекта other(v); } void general(string str) { //А из этой функции в функцию run передается строка other(str); } Что будет сгенерировано компилятором и как рассматривать не будем, т.к. это сильно зависит от компилятора и параметров компиляции, но постараемся посмотреть что может быть и как. В нашем случае other передает копию string_view, т.е. general копирует два указателя (не говорим о возможности вызова конструктора копирования, подразумевая, что он самый тривиальный и в результирующем коде приведет просто к копированию двух членов класса). Затем строка копируется из string_view. Ничего сложного. Теперь возьмем тот же код, но при этом run будет принимать ссылку на string. void run(string const & v) { other(v); } void general(string str) { other(str); //str используется, её нельзя перемещать в other } Да, Вы правы, передача ссылки в функцию run - быстрая операция, можно считать, что это копирование указателя. Но у передачи ссылки есть и минусы. Один из них - функция run обращается к строке через ссылку, т.е. появляется дополнительный уровень косвенности, который может вылиться в большие затраты, чем "лишнее" копирование указателя в версии функции, принимающей string_view. Теперь рассмотрим третий вариант функции run: void run(string v) { other(std::move(v)); } В данном случае копирование строки происходит еще в функции general (причем мы не перемещаем параметр str, т.к. он где-то там еще нужен далее). Затем функция run перемещает объект v в параметр функции other, а это в нашем случае приводит к копированию трех указателей и занулению старых. Очевидно, что в этом случае операций намного больше, чем со string_view, и, вполне вероятно, что косвенное обращение будет тоже быстрее. То есть это может быть самым тормознутым вариантом. А теперь представим, такую ситуацию: void run(string const &); //<-- имеется такая run void run(string_view); //или такая, нам без разницы class SomeClass { string m_text; mutex m_mutex; void SomeClass::call() { //Мы знаем, что run делает копию строки, //и запускает поток для её обработки lock_quard locker;//объект у нас защищен мьютексом run(m_text);//Где-то там строка копируется и запускается новый поток //мы не знаем точный момент, когда строка скопируется, //поэтому вынуждены ждать выполнения run под защитой мьютекса, //т.к. в other содержаться указатели на защищенные члены нашего класса. //Вполне вероятно, что в этот момент другие //потоки уже хотят работать с нашим объектом, //но не могут, т.к. мьютекс захвачен. } }; Я в комментариях описал проблему. А теперь возьмем ситуацию с копированием и перемещением: void run(string); //<-- теперь run принимает копию class SomeClass { //.. void SomeClass::call() { //Мы знаем, что run принимает копию строки //и запускает поток для её обработки unique_lock locker;//объект у нас защищен мьютексом string text_copy = m_text;//Под защитой выполняем копирование строки locker.unlock();//И разблокируем мьютекс, т.к. run(std::move(text_copy));//нам уже без разницы что-том делает run, //копия данных для него уже создана и другим потокам можно дать доступ к объекту } }; У string_view при этом тоже имеются свои прелести. string_view в принципе не привязан к string - это просто данные и их размер. Т.е. string_view может работать не только с string: void run(string_view v); void general(string str) { other(str);//ok } void general(char const * str, size_t len) { other(string_view(str, len));//ok } void general(char const * str) { other(string_view(str));//ok } void general(std::vector const & v) { other(string_view(v.data(), v.size()));//ok } В случае передачи ссылки придется сделать лишнюю копию строки, в случае передачи копии строки это не страшно, т.к. копия будет перемещена, но это всё равно может быть дороже. Однако, копия строки с перемещением может быть дешевле string_view, если, например для создания string_view потребуется сначала создать строку или какой-то другой буфер с данными. В таком случае будет лучше создать строку и переместить её. Но оптимизации также могут перевернуть всё с ног на голову. Когда нужно сделать копию строки, кажется, что это приведет к аллокации памяти и всё это будет медленно. В общем случае это верно, но есть такая штука, как SSO (small string optimization), которая позволяет хранить маленькие строки прямо в объекте string, используя сам объект как буфер для строки. В случае применения такой оптимизации копирование может оказаться равноценно или даже дешевле перемещения. То есть создав копию в вызывающей функции и переместив строку в вызываемую функцию, Вы если и потеряете скорость, то очень немного. Но это всё относится к ситуации, когда нужна копия на вызываемой стороне. Если же копия не нужна, то вариантов также масса. а для чего тогда стоит использовать string_view? string_view нужен как раз тогда, когда копия строки не нужна, но нужно сослаться на какое-то место и с ним работать как со строкой. Ранее нужно было либо писать свою "обертку" подобную string_view, либо копировать нужные данные из исходной строки и работать с ними. Всё сказанное выше весьма условно и служит только для демонстрации различных ситуаций. Как видите, применение того или иного средства полностью зависит от конкретных условий использования, применяемых алгоритмов, оптимизаций компилятора, устройства библиотеки, платформы на которой всё это работает и т.д., поэтому закончу тем, с чего начал - использование любого из альтернативных средств очень сильно зависит от ситуации, и выбирается исходя из конкретных условий.

Ответ 2



Просто std::string. А потом вместо компирования - перемещать через std::move.

Ответ 3



Если строка будет "просто скопирована", то вам следует реализовывать либо "ленивый" вариант семантики перемещения (передавать std::string по значению и затем делать из него перемещение), либо "полный" вариант семантики перемещения (писать две перегруженных функции: для const std::string & и для std::string &&), либо, возможно, реализовать forwarding (писать шаблонную функцию, принимающую универсальную сслыку и делающую std::forward в вашу копию). См. https://ru.stackoverflow.com/a/822789/182825 std::string_view уместен везде, где вы будете просто анализировать строку, т.е. он является заменителем const std:string & в ситуациях, когда копирование не будет делаться.

Комментариев нет:

Отправить комментарий