Edit1: 现在代码遵循“五规则”。问题依然存在。
Edit2: 现在只将 void* 传递给 printf 的
%p
。问题依然存在。
追踪一些代码中的分段错误,我注意到当一行像
Lexer* const lexer_;
对于存在的属性,代码崩溃;而如果没有
const
它工作顺利。
const
允许在上面的地方吗?
作为参考,下面是一个 C-Reduce'd C++ 代码,来自一个暴露问题的更大的程序。不幸的是,C-Reduce 在某些时候开始将标识符混淆为单个字母,所以我停止了缩减并试图让代码尽可能整洁。为了编译,我在 linux x86_64 上使用 g++ v11.3
> g++ main.cpp -o main.x -fsanitize=address -Werror=all -Werror=extra
运行,它打印
0x602000000010 = new Lexer
0x602000000030 = new Token
0x7ffca90b51f0 = new Expression
0x7ffca90b51f0 = start delete Expression
0x602000000010 = start delete Lexer
0x602000000030 = delete Token
0x602000000010 = done delete Lexer
=================================================================
==1232849==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000030 at pc 0x556fc889953d bp 0x7ffca90b5190 sp 0x7ffca90b5180
READ of size 8 at 0x602000000030 thread T0
#0 0x556fc889953c in ExpressionParser::Expression::~Expression() (.../main.x+0x153c)
...
0x602000000030 is located 0 bytes inside of 8-byte region [0x602000000030,0x602000000038)
freed by thread T0 here:
#0 0x7f5258f6f22f in operator delete(void*, unsigned long) .../libsanitizer/asan/asan_new_delete.cpp:172
#1 0x556fc889965f in ExpressionParser::Lexer::~Lexer() (.../main.x+0x165f)
...
previously allocated by thread T0 here:
#0 0x7f5258f6e1c7 in operator new(unsigned long) .../libsanitizer/asan/asan_new_delete.cpp:99
#1 0x556fc8899588 in ExpressionParser::Lexer::tokenize() (.../main.x+0x1588)
...
SUMMARY: AddressSanitizer: heap-use-after-free (/home/john/own/C/mp-gmp/const-problem/main-2.x+0x153c) in ExpressionParser::Expression::~Expression()
...
使用
-D CONST=
使得 lexer_
是非常量,代码运行良好并打印:
0x602000000010 = new Lexer
0x602000000030 = new Token
0x7ffff44937e0 = new Expression
0x7ffff44937e0 = start delete Expression
0x602000000010 = start delete Lexer
0x602000000030 = delete Token
0x602000000010 = done delete Lexer
0x7ffff44937e0 = end delete Expression
同样有效的是
virtual ~Lexer();
;由于Lexer
没有虚拟方法,所以不需要哪个?
#include <cstdio>
#ifndef CONST
#define CONST const
#endif
class ExpressionParser
{
public:
class Token;
class Lexer;
class Expression
{
friend ExpressionParser;
Expression (Token *token) : expression_(token)
{
printf ("%p = new Expression\n", (void*) this);
}
Expression (const Expression&) = delete;
Expression (Expression&&) = delete;
void operator= (const Expression&) = delete;
void operator= (Expression&&) = delete;
~Expression();
Token *expression_;
};
static void eval();
};
using EP = ExpressionParser;
class EP::Lexer
{
public:
Token *tokens_ = nullptr;
Lexer()
{
printf ("%p = new Lexer\n", (void*) this);
}
Lexer (const Lexer&) = delete;
Lexer (Lexer&&) = delete;
void operator= (const Lexer&) = delete;
void operator= (Lexer&&) = delete;
~Lexer();
void tokenize();
};
class EP::Token
{
friend ExpressionParser;
Lexer * CONST lexer_;
Token (Lexer *lexer) : lexer_(lexer)
{
printf ("%p = new Token\n", (void*) this);
}
Token (const Token&) = delete;
Token (Token&&) = delete;
void operator= (const Token&) = delete;
void operator= (Token&&) = delete;
~Token()
{
printf ("%p = delete Token\n", (void*) this);
}
};
void EP::eval()
{
Lexer *lexer = new Lexer();
lexer->tokenize();
(void) Expression (lexer->tokens_);
}
EP::Expression::~Expression()
{
printf ("%p = start delete Expression\n", (void*) this);
delete expression_->lexer_;
printf ("%p = end delete Expression\n", (void*) this);
}
void EP::Lexer::tokenize()
{
tokens_= new Token (this);
}
EP::Lexer::~Lexer()
{
printf ("%p = start delete Lexer\n", (void*) this);
delete tokens_;
printf ("%p = done delete Lexer\n", (void*) this);
}
int main (void)
{
ExpressionParser::eval();
}
看到这个已经重新开放了,我将发表我的评论作为答案:
使用 godbolt 的简化版本:https://godbolt.org/z/n1qzrWsdq
当
delete token->lexer;
完成时,析构函数 ~Lexer()
删除其标记,在本例中是 delete 语句中的 token
。此时,您正在删除一个仍在使用的指针,这将是未定义的行为。
从生成的程序集中,在非优化构建中:
mov rax, QWORD PTR [rbp-16]
mov rax, QWORD PTR [rax]
mov rdi, rax
call Lexer::~Lexer() [complete object destructor]
mov rax, QWORD PTR [rbp-16]
mov rax, QWORD PTR [rax]
mov esi, 8
mov rdi, rax
call operator delete(void*, unsigned long)
(其中
[rbp-16]
是token
的地址),可以看到after~Lexer()
被调用,token
被重新加载。
根据 GCC C++ 维护者的说法(正如 apple apple 已经在评论中指出的那样),这是一个自 2012 / v4.6 以来已知的 GCC 错误,即 PR52339。它已经存在于 v4.0 中,但也可以在当前的主控(未来的 v14)或 v11.3 中重现。原因是最后的
delete
中的表达式被求值不止一次,与[expr.delete]:冲突
4delete-expression 中的 cast-expression 应恰好求值一次。
测试用例:
struct Lexer;
struct Token
{
Lexer* const lexer_;
Token (Lexer *l) : lexer_(l) {}
~Token() = default;
Token() = delete;
Token (const Token&) = delete;
Token (Token&&) = delete;
void operator= (const Token&) = delete;
void operator= (Token&&) = delete;
};
struct Lexer
{
Token *token_;
Lexer() = default;
~Lexer() { delete token_; }
Lexer (const Lexer&) = delete;
Lexer (Lexer&&) = delete;
void operator= (const Lexer&) = delete;
void operator= (Lexer&&) = delete;
};
int main()
{
Lexer *lexer = new Lexer();
Token *token = new Token (lexer);
lexer->token_ = token;
delete token->lexer_;
// delete lexer; // is OK
}
命令行
$ g++ main-3.cpp -O2 && ./a.out
但也被
-O0
或 -m32
触发。