Есть ли недостатки использования std :: string в качестве буфера?

Недавно я видел своего коллегу, использующего 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 это не вредит, пока мы знаем, что мы делаем, но я что-то пропустил? Это опасно?

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Текущий код может иметь неопределенное поведение, в зависимости от версии 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_of и find_first_not_of. Это было удобно при поиске по массивам байтов.

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


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

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

Я точно знаю, что это не безопасно в 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. 0xXXXXXX41 3 байта были мусором, поэтому 32-битный регистр был 0xXXXXXX41. Когда я разыменовался в ptr[0], GCC разыменовал адрес мусора 0xXXXXXX41.

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

Ответ 4

С С++ 17 data могут возвращать неконстантный char *.

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

const charT* c_str() const noexcept;
const charT* data() const noexcept;
....
charT* data() noexcept;

Ответ 5

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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