[clang] [Clang] Restrict CWG1504 devirtualization to pointer arithmetic on record types. (PR #209593)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Jul 15 07:28:37 PDT 2026
https://github.com/AZero13 updated https://github.com/llvm/llvm-project/pull/209593
>From 8e72ccb0cb168bda615273b61ea9de633ded4350 Mon Sep 17 00:00:00 2001
From: AZero13 <gfunni234 at gmail.com>
Date: Tue, 14 Jul 2026 14:57:42 -0400
Subject: [PATCH] [Clang] Restrict CWG1504 devirtualization to pointer
arithmetic on record types
Fixes a regression introduced in PR #207540 where virtual calls (including destructor calls in delete expressions) were incorrectly devirtualized on pointers obtained from pointer arithmetic on pointers (e.g. array-of-pointers subscripts).
---
clang/lib/AST/DeclCXX.cpp | 20 +++++++
clang/lib/AST/Expr.cpp | 58 +++++++++++++++++++
clang/test/CXX/drs/cwg15xx.cpp | 19 ++++++
.../devirtualize-virtual-function-calls.cpp | 58 ++++++++++++++++++-
clang/www/cxx_dr_status.html | 2 +-
5 files changed, 154 insertions(+), 3 deletions(-)
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index 0573cdf95952a..f1bc1a3283e5f 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -2600,6 +2600,26 @@ CXXMethodDecl *CXXMethodDecl::getDevirtualizedMethod(const Expr *Base,
}
}
+ // By CWG1504 / C++11 [expr.add]p6, pointer arithmetic on a base pointer into
+ // an array of derived objects is undefined behavior when the element type and
+ // pointee type are not similar. This means we can devirtualize calls on
+ // objects accessed through array subscripts or pointer arithmetic with
+ // non-zero offsets, since the dynamic type must match the static type.
+ // Expr::getBestDynamicClassTypeExpr evaluates this and will not strip
+ // the array subscript or pointer arithmetic if the offset is non-zero.
+ if (isa<ArraySubscriptExpr>(Base) ||
+ (isa<UnaryOperator>(Base) &&
+ cast<UnaryOperator>(Base)->getOpcode() == UO_Deref)) {
+ return DevirtualizedMethod;
+ }
+
+ if (const auto *BO = dyn_cast<BinaryOperator>(Base)) {
+ if (BO->getType()->isPointerType() &&
+ BO->getType()->getPointeeType()->isRecordType() &&
+ (BO->getOpcode() == BO_Add || BO->getOpcode() == BO_Sub))
+ return DevirtualizedMethod;
+ }
+
// We can't devirtualize the call.
return nullptr;
}
diff --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp
index 0bb9c6aa01c39..4532a67412bdd 100644
--- a/clang/lib/AST/Expr.cpp
+++ b/clang/lib/AST/Expr.cpp
@@ -61,6 +61,64 @@ const Expr *Expr::getBestDynamicClassTypeExpr() const {
continue;
}
+ // CWG1504: Pointer arithmetic on an array of objects.
+ // If the index is zero or unknown, we cannot prove the dynamic type.
+ // So we strip the array subscript and evaluate the base.
+ if (auto *ASE = dyn_cast<ArraySubscriptExpr>(E)) {
+ if (const RecordDecl *RD = ASE->getType()->getAsRecordDecl()) {
+ Expr::EvalResult Result;
+ if (!ASE->getIdx()->EvaluateAsInt(Result, RD->getASTContext()) ||
+ Result.Val.getInt().isZero()) {
+ E = ASE->getBase();
+ continue;
+ }
+ }
+ }
+
+ // Same for pointer arithmetic with deref: *(p + N)
+ if (auto *UO = dyn_cast<UnaryOperator>(E)) {
+ if (UO->getOpcode() == UO_Deref) {
+ if (const RecordDecl *RD = UO->getType()->getAsRecordDecl()) {
+ const Expr *Inner = UO->getSubExpr()->IgnoreParenImpCasts();
+ if (auto *BO = dyn_cast<BinaryOperator>(Inner)) {
+ if (BO->getOpcode() == BO_Add || BO->getOpcode() == BO_Sub) {
+ bool PointerOnLHS = BO->getLHS()->getType()->isPointerType();
+ const Expr *IdxExpr = PointerOnLHS ? BO->getRHS() : BO->getLHS();
+ Expr::EvalResult Result;
+ if (!IdxExpr->EvaluateAsInt(Result, RD->getASTContext()) ||
+ Result.Val.getInt().isZero()) {
+ E = UO->getSubExpr();
+ continue;
+ }
+ } else {
+ E = UO->getSubExpr();
+ continue;
+ }
+ } else {
+ E = UO->getSubExpr();
+ continue;
+ }
+ }
+ }
+ }
+
+ // Same for pointer arithmetic with arrow: (p + N)->f()
+ if (auto *BO = dyn_cast<BinaryOperator>(E)) {
+ if ((BO->getOpcode() == BO_Add || BO->getOpcode() == BO_Sub) &&
+ BO->getType()->isPointerType()) {
+ if (const RecordDecl *RD = BO->getType()->getPointeeType()->getAsRecordDecl()) {
+ bool PointerOnLHS = BO->getLHS()->getType()->isPointerType();
+ const Expr *IdxExpr = PointerOnLHS ? BO->getRHS() : BO->getLHS();
+ Expr::EvalResult Result;
+ if (!IdxExpr->EvaluateAsInt(Result, RD->getASTContext()) ||
+ Result.Val.getInt().isZero()) {
+ E = PointerOnLHS ? BO->getLHS() : BO->getRHS();
+ continue;
+ }
+ }
+ }
+ }
+
break;
}
diff --git a/clang/test/CXX/drs/cwg15xx.cpp b/clang/test/CXX/drs/cwg15xx.cpp
index 5a9b80ed028c4..b9e0525772779 100644
--- a/clang/test/CXX/drs/cwg15xx.cpp
+++ b/clang/test/CXX/drs/cwg15xx.cpp
@@ -11,6 +11,25 @@
// cxx98-error at -1 {{variadic macros are a C99 feature}}
#endif
+namespace cwg1504 { // cwg1504: 3.1
+#if __cplusplus >= 201103L
+ // CWG1504: Pointer arithmetic after derived-base conversion
+ struct Base { int x; };
+ struct Derived : Base { int y; };
+ constexpr Derived arr[2] = {};
+
+ // Pointer arithmetic on a base pointer into a derived array is UB,
+ // and the constexpr evaluator must diagnose it.
+ constexpr int test(int n) {
+ return ((const Base*)arr)[n].x; // #cwg1504-x
+ }
+ constexpr int bad = test(1);
+ // since-cxx11-error at -1 {{constexpr variable 'bad' must be initialized by a constant expression}}
+ // since-cxx11-note@#cwg1504-x {{cannot access field of pointer past the end of object}}
+ // since-cxx11-note at -3 {{in call to 'test(1)'}}
+#endif
+} // namespace cwg1504
+
namespace cwg1512 { // cwg1512: 4
void f(char *p) {
if (p > 0) {}
diff --git a/clang/test/CodeGenCXX/devirtualize-virtual-function-calls.cpp b/clang/test/CodeGenCXX/devirtualize-virtual-function-calls.cpp
index b50881db63e05..dac2c2b183d7d 100644
--- a/clang/test/CodeGenCXX/devirtualize-virtual-function-calls.cpp
+++ b/clang/test/CodeGenCXX/devirtualize-virtual-function-calls.cpp
@@ -92,10 +92,48 @@ void fd(D d, XD xd, D *p) {
// CHECK: call void %
p[0].f();
- // FIXME: We can devirtualize this, by C++1z [expr.add]/6 (if the array
+ // We can devirtualize this, by CWG1504 / [expr.add]/6 (if the array
// element type and the pointee type are not similar, behavior is undefined).
- // CHECK: call void %
+ // CHECK: call void @_ZN1A1fEv
p[1].f();
+
+ // Negative indices are also UB for the same reason.
+ // CHECK: call void @_ZN1A1fEv
+ p[-1].f();
+
+ // Pointer arithmetic with arrow syntax: (p + N)->f()
+ // CHECK: call void @_ZN1A1fEv
+ (p + 1)->f();
+
+ // Pointer subtraction with arrow syntax: (p - N)->f()
+ // CHECK: call void @_ZN1A1fEv
+ (p - 1)->f();
+
+ // Can't devirtualize with non-constant index; we can't prove N != 0.
+ int n = 1;
+ // CHECK: call void %
+ p[n].f();
+
+ // Zero through expression: p[1-1] evaluates to 0, can't devirtualize.
+ // CHECK: call void %
+ p[1-1].f();
+
+ // (p + 0)->f() also can't be devirtualized (same as *p).
+ // CHECK: call void %
+ (p + 0)->f();
+
+ // 1 + p is legal pointer arithmetic too.
+ // CHECK: call void @_ZN1A1fEv
+ (1 + p)->f();
+
+ // Constant variables are evaluated.
+ const int N = 1;
+ // CHECK: call void @_ZN1A1fEv
+ p[N].f();
+
+ // Pointer arithmetic with deref: *(p + 1)
+ // CHECK: call void @_ZN1A1fEv
+ (*(p + 1)).f();
}
struct B {
@@ -195,3 +233,19 @@ namespace test5 {
q.f();
}
}
+
+namespace test6 {
+ struct Thing {
+ virtual ~Thing();
+ virtual void f();
+ };
+
+ // CHECK-LABEL: define {{.*}} @_ZN5test63fooEPPNS_5ThingE
+ void foo(Thing **instances) {
+ // CHECK: call void %
+ instances[1]->f();
+
+ // CHECK: call void %
+ delete instances[1];
+ }
+}
diff --git a/clang/www/cxx_dr_status.html b/clang/www/cxx_dr_status.html
index af91ac559d274..ffa16dc066bfe 100755
--- a/clang/www/cxx_dr_status.html
+++ b/clang/www/cxx_dr_status.html
@@ -10309,7 +10309,7 @@ <h2 id="cxxdr">C++ defect report implementation status</h2>
<td>[<a href="https://wg21.link/expr.add">expr.add</a>]</td>
<td>CD3</td>
<td>Pointer arithmetic after derived-base conversion</td>
- <td class="unknown" align="center">Unknown</td>
+ <td class="full" align="center">Clang 3.1</td>
</tr>
<tr id="1505">
<td><a href="https://cplusplus.github.io/CWG/issues/1505.html">1505</a></td>
More information about the cfe-commits
mailing list