Google Sparsehash использует realloc () для типа, который нетривиально копируется

Рассмотрим эту простую программу:

#include <string>
#include <sparsehash/dense_hash_map>

int main()
{
google::dense_hash_map<std::string, int> map;
map["foo"] = 0;
}

Компиляция с GCC 8.2 и -Wclass-memaccess (или же -Wall) выдает предупреждение:

sparsehash/internal/libc_allocator_with_realloc.h:68:40: warning:
‘void* realloc(void*, size_t)’ moving an object of non-trivially copyable type
‘struct std::pair<const std::__cxx11::basic_string<char>, int>’;
use ‘new’ and ‘delete’ instead [-Wclass-memaccess]
return static_cast<pointer>(realloc(p, n * sizeof(value_type)));
~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~

Вопросы:

  1. Это неопределенное поведение?
  2. Можете ли вы предложить исправление или обходной путь, который можно применить к коду приложения (не изменяя Sparsehash или избегая его использования)?
  3. (Бонусные баллы) Можете ли вы создать программу, которая на самом деле ведет себя неправильно из-за этого (используя std :: string или ваш собственный нетривиальный тип)? До сих пор я не видел никаких проблем в коде, использующем std :: string в качестве типа ключа, несмотря на тот факт, что std :: string должен быть довольно распространенным типом ключа.

Я подал вопрос здесь: https://github.com/sparsehash/sparsehash/issues/149

5

Решение

  1. Да, это неопределенное поведение.
    Но пока не отчаивайся, если std::string не хранит никаких внутренних указателей в вашей реализации и нигде не регистрирует их, это все равно будет «работать»; создание побитовой копии будет эквивалентно построению перемещения в месте назначения и уничтожению источника.
    Так обстоит дело для большинства (не все) строковые реализации, SSO или нет.

  2. Если вы можете использовать тип, который не гарантированно является тривиально-разрушительно-перемещаемым, используйте другой распределитель (последний аргумент шаблона), чтобы избежать побитовых перемещений.

  3. Взрыв программы из-за неправильного перемещения по битовой копии тривиален.
    Используйте этот тип с google::dense_hash_map:

    class bang {
    bang* p;
    public:
    bang() : p(this) {}
    bang(bang const&) : bang() {}
    bang& operator=(bang const&) { return *this; }
    ~bang() { if (p != this) std::abort(); }
    };
    
1

Другие решения

1. Это неопределенное поведение?
Да. Вы никогда не должны копировать объекты, используя realloc (), потому что иногда они имеют внутренние указатели, которые указывают на ресурс. Проблема возникает позже, когда на двух разных объектах работают деструкторы. Теперь происходит двойное освобождение тот же ресурс, полное нет нет.

2. Можете ли вы предложить исправление или обходной путь, который можно применить к коду приложения (не изменяя Sparsehash или избегая его использования)?

Пытаться

#include <memory>

и изменить строку

google::dense_hash_map<std::string, int> map;

в

google::dense_hash_map<std::string, int, std::hash<std::string>, std::equal_to<std::string>, std::allocator> map;

Теперь он не будет использовать распределитель Google libc_allocator_with_realloc

3. (Бонусные баллы) Вы можете создать программу, которая на самом деле ведет себя неправильно из-за этого (используя std :: string или ваш собственный нетривиальный тип)? До сих пор я не видел никаких проблем в коде, использующем std :: string в качестве типа ключа, несмотря на тот факт, что std :: string должен быть довольно распространенным типом ключа.

Не легко. Потому что вы пытаетесь вызвать неопределенное поведение. В вашей тестовой программе я бы передавал строки длиной не менее 32 символов, поэтому оптимизация небольших строк не запускается. И в куче gcc можно выполнить тесты, чтобы проверить, не испортилась ли она. Увидеть
1

2

Я предполагаю, что этот код ожидает атрибут класса C ++ 20 легко перемещаемый. По сути, это объект, местоположение памяти которого можно смело менять. На языке c ++ это объект, который можно безопасно скопировать, копируя представление объекта, и программа сохраняет ожидаемое поведение до тех пор, пока к копируемому объекту больше нет доступа, даже для уничтожения.

Например, этот код не может быть указан как «неопределенное поведение» стандартом C ++ 20:

alignas(string) unsigned char buffer[sizeof(string)];
auto p = new(buffer) string{"test"};
alignas(string) unsigned char buffer2[sizeof(string)];
memcpy(buffer,buffer2,sizeof(string));//create a new string object copy of *p;
auto np = reinterpret_cast<string*>(buffer2);
(*np)[0]="r";
// the object at p shall not be accessed, not even destroyed.

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

struct fail{
int a;
int b;
int* selected;
fail(bool is_a,int i){
if (is_a){ a=i; selected=&a;}
else { b=i; selected=&b;}
}
};

Некоторая реализация контейнера связанного списка также не может быть легко перемещаемой, например, если контейнер содержит элемент, который является корневым узлом. Так dense_hash_map просто не должны использоваться с такими типами ссылок на собственную память.

0
По вопросам рекламы [email protected]