4

记 C++开发中的一个小坑

 2 years ago
source link: https://www.v2ex.com/t/835428
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

V2EX  ›  C++

记 C++开发中的一个小坑

  dangyuluo · 10 小时 55 分钟前 · 1043 次点击

最近写的功能里,发现一个单元测试在 Debug 下可以通过,但是在 RelWithDebInfo 下却报错。错误发生在使用memcmp比较两个内存地址处。抽象出来的代码如下

struct GID{
  MyType m_type; // Member with alignment as 8
  bool used; // bool has alignment of 1
};


GID gid1;
...
GID gid2 = gid1;

assert(memcmp(&gid1, &gid2, sizeof(GID)) == 0); // failed for RelWithDebInfo

后来经过排查,发现 Debug 时gid2.used成员之后内存是干净的,padding 均为 0 ,但是在 RenWithDebInfo 下gid2.used之后内存却有随机值,导致了memcmp失败。经过搜索发现了 auto-generated copy constructor 是不会将 padding 置零。读了 memcmp 的文档后也发现已经有提示过了。真是学无止境。

https://stackoverflow.com/questions/70979077/does-c-standard-guarantee-the-initialization-of-padding-bytes-to-zero-for-non

https://en.cppreference.com/w/cpp/string/byte/memcmp

15 条回复    2022-02-22 01:56:35 +08:00

wctml

wctml      10 小时 31 分钟前

楼主 你太年轻了。(❁´◡`❁)

Kasumi20

Kasumi20      10 小时 20 分钟前

正确的,没必要折腾几乎不访问的内存。比较结构就得自己实现,很多成员都是指针,指向别的结构

wuruxu

wuruxu      10 小时 15 分钟前

这些变量都应该初始化下 比如 memset, bzero

jones2000

jones2000      10 小时 8 分钟前

1. 变量或结构体要初始化
2. 结构体字节对齐

LifStge

LifStge      9 小时 22 分钟前

最关键的问题 不是模式问题 跟 memcmp 也没太大关系 (虽然用来这样比较两个结构存在非常大的问题,正常情况自己做操作符重载)
关键在于 初始化的问题 主要原因 c++ 隐式的行为太多了 这次的问题 属于开发工具 为了 debug 模式下 查找问题容易些 在 c++标准之外做了额外的处理 导致了 本来未知行为的代码(也就是错误) 变成了固定行为(也就是 memcmp 比较的内存相等 )
https://en.cppreference.com/w/cpp/language/default_initialization#:~:text=otherwise%2C%20no%20initialization%20is%20performed%3A%20the%20objects%20with%20automatic%20storage%20duration%20(and%20their%20subobjects)%20contain%20indeterminate%20values.

https://en.cppreference.com/w/cpp/language/value_initialization

https://en.cppreference.com/w/cpp/language/zero_initialization

很多时候都会说 声明变量的时候 就做主动初始化的操作 就比如 T{} T (...)等 这也是个好的习惯 但是很多时候 这种也是属于多余的操作 这就要看自己对代码的把控了
反正就是 切勿对未知的内存做操作就行了 更多的情况 不要关注 debug 模式下可以 release 模式下又不可以 再比如 gcc 下可以 msvc 下又不可以...... 只以标准规定的来做

c++标准这东西 还是多写代码吧 做的多了 就慢慢熟悉了 不过嘛 平常一定按照标准规定的流程做 总没错的
虽然其他的方法也能解决问题 但是也容易带来很多问题
总结就是 该初始化就初始化 把控好 c++的各种隐式的默认行为 能使用明确的 就不要依赖隐式默认的

Mithril

Mithril      9 小时 7 分钟前

C++主要问题是给码农的权限太高了,随随便便你就可以瞎搞内存。
我见过一个奇葩的就是有个数组每次都访问越界,不过其实越界了多数情况下也没什么问题,两个函数以后就切掉了。
但是就只有越界的时候恰好后面的内存里是某些特殊符号就会出问题。
所以就谁分到那块内存谁倒霉,表现就是随机炸。你最后 Debug 去吧,鬼知道哪天才能复现。
查出这个问题的时候也是运气好,恰好挂着调试器,它恰好炸了。。。

jim9606

jim9606      9 小时 0 分钟前

struct alignment 和 padding 的问题在 C 一样会有吧。

@jones2000 @LifStge
这事情跟没有初始化也没啥关系吧,这里问题是用了错误的比较方法。GID gid2=gid1 就是 well-defined 的初始化。懒得写比较函数的可以看看显式声明比较函数能不能用而不是用 memcmp 这种依赖 UB 的行为。

lakehylia

lakehylia      8 小时 58 分钟前

真要搞这种字节对比,就应该给 struct 加上 #pragma pack(1)

bitdepth

bitdepth      8 小时 28 分钟前

我反而懷疑是 MyType m_type 這個 MyType 的問題

newmlp

newmlp      8 小时 15 分钟前

不是应该重载==做判断吗,怎么还能用内存判断相等

LifStge

LifStge      6 小时 40 分钟前

@jim9606 这种比较方法的使用在大部分情况下是有问题的 这是可以确认的
最终原因就未初始化的结果 是未知的 op 也贴了问题所在 这种隐式的默认行为编译器也有差异问题 之所以 debug 模式下没问题 归根就是编译器的特殊处理的结果 其实也没必要讨论的 明显就是用错了 C4700 的警告 最新的 vc++下是视为错误的 g++ clang++ 倒是没错 但是这本身就是未知的行为 所以实现也有各自的差异 但是把个别编译器对这种未知行为的默认实现 当作标准来用就带来的 现在的问题
```c++
#include <iostream>
#pragma pack(8)
struct AA
{
bool m;
int k;
bool d;
};

int main()
{
AA a;

AA b =a;

for (int i = 0; i < sizeof(a); i++)
{
std::cout << std::hex << (unsigned int)(((unsigned char*)&a)[i]) << ",";
}
std::cout << std::endl;

for (int i = 0; i < sizeof(a); i++)
{
std::cout << std::hex << (unsigned int)(((unsigned char*)&b)[i]) << ",";
}
std::cout << std::endl;

return 0;
}
```
a 未初始化结果
g++ -Os main.cpp
0,0,0,0,0,0,0,0,80,22,2,91,
0,0,0,0,0,0,0,0,80,22,2,91,

clang++ -Os main.cpp
b4,34,e2,4b,ff,7f,0,0,0,0,0,0,
c0,12,40,0,0,0,0,0,d0,10,40,0,

如果把 a 做零初始化后 AA a{}; AA b=a;
g++ -Os main.cpp
0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,

clang++ -Os main.cpp
0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,

初始化后的结果是一样的

也不是说必须要主动的做零初始化 毕竟很多时候 做零初始化是多余的 不过就是很多时候是依赖编译器优化 还是自己主动优化 自己手动优化就要注意更多的细节

总结就是 对结构跟类用 memcmp 做比较是存在问题的
对未初始化的变量的使用上 是要注意的 不要依赖编译器对这种未知行为的实现 因为本身就是未知的行为 不是语言标准的规定 所以编译器实现上也没有必要的统一实现 结果上不同正常的 但是不要把对这种未知行为的实现 当成语言的标准来使用 (除非限定使用前提 ) 比如 此代码 必须在 g++ 甚至指定版本 指定 debug 下运行....

kirory

kirory      5 小时 55 分钟前

既然是 C++, 不是 operator < / <=> 就完了吗

jim9606

jim9606      1 小时 54 分钟前

@LifStge
可能我语序有点问题,我的意思是复制构造(初始化)是没有问题的,比较有问题。一般想到用 memcmp 做比较的起因是懒得手写一个逐成员比较的比较函数,或者想提高性能。如果目的只是前者的话,C++20 可以显式定义默认比较函数。如果成员都是平凡可比较的话用这个默认比较函数的话是没有问题的,不会出现 OP 所提的问题。

https://zh.cppreference.com/w/cpp/language/default_comparisons

dangyuluo

dangyuluo      1 小时 36 分钟前

@jim9606 你说得对,这确实是我犯懒了,而且这个测试的本意是某个对象被拷贝后内存一致,我没想到 padding

trivial 翻译成平凡确实是我没想到的😂

littlewing

littlewing      1 小时 7 分钟前

@Mithril 这其实就是一种性能和安全性的权衡了,std::array 中如果用 [] 访问同样也不做越界检查,如果用 at() 就会做越界检查,其他语言比如 rust 中也有同样的设计

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK