Google Sparsehash 在不可平凡复制的类型上使用 realloc()
Google Sparsehash uses realloc() on type which is not trivially copyable
考虑这个简单的程序:
#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)));
~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
问题是:
- 这是未定义的行为吗?
- 您能否建议可应用于应用程序代码的修复或解决方法(不是通过更改 Sparsehash 或避免使用它)?
- (奖励积分)你能构建一个实际上因此行为不当的程序(使用 std::string 或你自己的非平凡类型)吗?到目前为止,我还没有看到使用 std::string 作为键类型的代码有任何问题,尽管 std::string 必须是一种非常常用的键类型。
我在这里提交了一个问题:https://github.com/sparsehash/sparsehash/issues/149
1.这是未定义的行为吗?
是的。你不应该使用 realloc() 复制对象,因为有时它们有指向资源的内部指针。当 2 个不同的对象具有它们的析构函数时,问题会在稍后出现 运行。现在同一个资源发生双重释放,完全没有。
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
是的,这是未定义的行为。
但是不要绝望,如果 std::string
没有在你的实现中存储任何内部指针,也没有在任何地方注册它们,无论如何它都会 "work" ;进行按位复制将等同于在目的地移动构造并破坏源。
大多数(不是所有)字符串实现都是这种情况,无论是否是 SSO。
如果您可能使用不保证可破坏性移动的类型,请使用不同的分配器(最后一个模板参数)来避免按位移动。
由于按位复制的无效移动而导致程序崩溃是微不足道的。
将此类型与 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(); }
};
我想这段代码预期可能是 c++20 class 属性 trivially relocatable。本质上,这是一个可以安全更改内存位置的对象。用 c++ 的说法,这是一个可以通过复制对象表示来安全复制的对象,只要复制的对象不再被访问,程序就会保持预期的行为,甚至不会被销毁。
例如,C++20 标准可能不会将此代码指定为 "undefined behavior":
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
不应与那些自内存引用类型一起使用。
考虑这个简单的程序:
#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)));
~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
问题是:
- 这是未定义的行为吗?
- 您能否建议可应用于应用程序代码的修复或解决方法(不是通过更改 Sparsehash 或避免使用它)?
- (奖励积分)你能构建一个实际上因此行为不当的程序(使用 std::string 或你自己的非平凡类型)吗?到目前为止,我还没有看到使用 std::string 作为键类型的代码有任何问题,尽管 std::string 必须是一种非常常用的键类型。
我在这里提交了一个问题:https://github.com/sparsehash/sparsehash/issues/149
1.这是未定义的行为吗? 是的。你不应该使用 realloc() 复制对象,因为有时它们有指向资源的内部指针。当 2 个不同的对象具有它们的析构函数时,问题会在稍后出现 运行。现在同一个资源发生双重释放,完全没有。
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
是的,这是未定义的行为。
但是不要绝望,如果std::string
没有在你的实现中存储任何内部指针,也没有在任何地方注册它们,无论如何它都会 "work" ;进行按位复制将等同于在目的地移动构造并破坏源。
大多数(不是所有)字符串实现都是这种情况,无论是否是 SSO。如果您可能使用不保证可破坏性移动的类型,请使用不同的分配器(最后一个模板参数)来避免按位移动。
由于按位复制的无效移动而导致程序崩溃是微不足道的。
一起使用
将此类型与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(); } };
我想这段代码预期可能是 c++20 class 属性 trivially relocatable。本质上,这是一个可以安全更改内存位置的对象。用 c++ 的说法,这是一个可以通过复制对象表示来安全复制的对象,只要复制的对象不再被访问,程序就会保持预期的行为,甚至不会被销毁。
例如,C++20 标准可能不会将此代码指定为 "undefined behavior":
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
不应与那些自内存引用类型一起使用。