Есть ли недостатки использования std :: string в качестве буфера? - PullRequest
68 голосов
/ 03 июня 2019

Я недавно видел, как мой коллега использовал std::string в качестве буфера:

std::string receive_data(const Receiver& receiver) {
  std::string buff;
  int size = receiver.size();
  if (size > 0) {
    buff.resize(size);
    const char* dst_ptr = buff.data();
    const char* src_ptr = receiver.data();
    memcpy((char*) dst_ptr, src_ptr, size);
  }
  return buff;
}

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

Это выглядит немного странно для меня, так как согласно cplusplus.com метод data() возвращает const char*, указывая на буфервнутренне управляемый строкой:

const char* data() const noexcept;

Запоминание константного указателя на символ ?AFAIK это не вредит, пока мы знаем, что мы делаем, но я что-то пропустил?Это опасно?

Ответы [ 7 ]

70 голосов
/ 03 июня 2019

Не используйте std::string в качестве буфера.

Неправильно использовать std::string в качестве буфера по нескольким причинам (перечисленным в произвольном порядке):

  • std::string не предназначался для использования в качестве буфера;вам нужно было бы еще раз проверить описание класса, чтобы убедиться в отсутствии «ошибок», которые бы препятствовали определенным шаблонам использования (или заставляли их вызывать неопределенное поведение).
  • В качестве конкретного примера: перед C ++17, вы даже не можете написать через указатель, который вы получаете с помощью data() - это const Tchar *;поэтому ваш код будет вызывать неопределенное поведение.(Но &(str[0]), &(str.front()) или &(*(str.begin())) будут работать.)
  • Использование std::string s для буферов сбивает с толку читателей реализации, которые предполагают, что вы будете использовать std::string для,ну струны.Другими словами, это нарушает принцип наименьшего удивления .
  • Хуже того, это сбивает с толку тех, кто может использовать эту функцию - они тоже могут думать о том, что вы 'возвращение - это строка, то есть допустимый читаемый человеком текст.
  • std::unique_ptr подойдет для вашего случая, или даже std::vector.В C ++ 17 вы также можете использовать std::byte для типа элемента.Более сложным вариантом является класс с SSO -подобной функцией, например, Boost's small_vector (спасибо, @ gast128, за упоминание об этом).
  • (Незначительный момент :) libstdc ++ пришлось изменить свой ABI для std::string, чтобы он соответствовал стандарту C ++ 11, что в некоторых случаях (что в настоящее время довольно маловероятно), вы можете столкнуться с некоторыми связями или проблемами времени выполнения что бы вы не использовали другой тип для вашего буфера.

Кроме того, ваш код может сделать два выделения вместо одной кучи (зависит от реализации): один раз при построении строки и другойкогда resize() инг.Но это само по себе не является причиной для того, чтобы избегать std::string, поскольку вы можете избежать двойного распределения, используя конструкцию из @ Jarod42's answer .

64 голосов
/ 03 июня 2019

Вы можете полностью избежать руководства memcpy, вызвав соответствующий конструктор:

std::string receive_data(const Receiver& receiver) {
    return {receiver.data(), receiver.size()};
}

, который даже обрабатывает \0 в строке.

Кстати, если содержимое не является фактически текстомЯ бы предпочел std::vector<std::byte> (или эквивалент).

9 голосов
/ 03 июня 2019

Запоминание константного указателя на символ?Насколько нам известно, это не вредит, если мы знаем, что мы делаем, но это хорошее поведение и почему?

Текущий код может иметь неопределенное поведение, в зависимости от версии C ++.Чтобы избежать неопределенного поведения в C ++ 14 и ниже, возьмите адрес первого элемента.Он дает неконстантный указатель:

buff.resize(size);
memcpy(&buff[0], &receiver[0], size);

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

Это было несколько распространено в старом коде, особенно в C ++ 03.Есть несколько преимуществ и недостатков использования такой строки.В зависимости от того, что вы делаете с кодом, std::vector может быть немного анемичным, и вы вместо этого иногда используете строку и принимаете дополнительные издержки char_traits.

Например, std::string обычноболее быстрый контейнер, чем std::vector при добавлении, и вы не можете вернуть std::vector из функции.(Или вы не могли сделать это на практике в C ++ 98, потому что C ++ 98 требовал, чтобы вектор был создан в функции и скопирован).Кроме того, std::string позволяет выполнять поиск с более широким ассортиментом функций-членов, таких как find_first_of и find_first_not_of.Это было удобно при поиске по массивам байтов.

Я думаю, что вы действительно хотите / нуждаетесь в классе веревки SGI , но он никогда не попадал в STL.Похоже, что GCC libstdc ++ может предоставить его.


Существует долгая дискуссия о том, что это допустимо в C ++ 14 и ниже:

const char* dst_ptr = buff.data();
const char* src_ptr = receiver.data();
memcpy((char*) dst_ptr, src_ptr, size);

Iзнаю наверняка, это не безопасно в GCC.Однажды я сделал что-то подобное в некоторых тестах, и это привело к ошибке:

std::string buff("A");
...

char* ptr = (char*)buff.data();
size_t len = buff.size();

ptr[0] ^= 1;  // tamper with byte
bool tampered = HMAC(key, ptr, len, mac);

GCC поместил один байт 'A' в регистр AL.Старшие 3 байта были мусором, поэтому 32-битный регистр был 0xXXXXXX41.Когда я разыменовался в ptr[0], GCC разыменовал адрес мусора 0xXXXXXX41.

Для меня были взяты две вещи: не пишите самопроверки и не пытайтесь сделать data() неконстантный указатель.

7 голосов
/ 03 июня 2019

Код не нужен, учитывая, что

std::string receive_data(const Receiver& receiver) {
    std::string buff;
    int size = receiver.size();
    if (size > 0) {
        buff.assign(receiver.data(), size);
    }
    return buff;
}

будет делать то же самое.

7 голосов
/ 03 июня 2019

Из C ++ 17, data может вернуть неконстантный char *.

Черновик n4659 объявляется в [string.accessors]:

const charT* c_str() const noexcept;
const charT* data() const noexcept;
....
charT* data() noexcept;
5 голосов
/ 04 июня 2019

Большая возможность оптимизации, которую я бы здесь исследовал, состоит в следующем: Receiver, по-видимому, является неким контейнером, который поддерживает .data() и .size().Если вы можете использовать его и передать его в качестве ссылки rvalue Receiver&&, вы сможете использовать семантику перемещения, не создавая вообще никаких копий!Если у него есть интерфейс итератора, вы можете использовать его для конструкторов на основе диапазона или std::move() из <algorithm>.

В C ++ 17 (как упомянули Серж Баллеста и другие), std::string::data() возвращаетуказатель на неконстантные данные.std::string гарантированно хранит все свои данные непрерывно в течение многих лет.

Написанный код пахнет немного, хотя на самом деле это не ошибка программиста: эти хаки были необходимы в то время.Сегодня вы должны как минимум изменить тип dst_ptr с const char* на char* и удалить приведение в первом аргументе к memcpy().Вы также можете reserve() количество байтов для буфера и затем использовать функцию STL для перемещения данных.

Как уже упоминалось, std::vector или std::unique_ptr будет более естественной структурой данныхиспользовать здесь.

4 голосов
/ 06 июня 2019

Одним из недостатков является производительность. Метод .resize по умолчанию инициализирует все новые местоположения байтов в 0. Эта инициализация не нужна, если вы собираетесь переписать 0 другими данными.

...