[clang] [Clang][OpenMP] Fix '*' reduction identity for class types (PR #202292)
Dmitry Mikushin via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 8 01:57:34 PDT 2026
https://github.com/dmikushin created https://github.com/llvm/llvm-project/pull/202292
Clang accepts OpenMP built-in reductions over C++ class types such as std::complex<T> as an extension (the OpenMP standard only defines the implicitly-declared reduction identities for arithmetic and, in Clang, for _Complex types; GCC rejects the construct outright). For such class types the private reduction copy was left value-initialized, which yields the *additive* identity (e.g. std::complex(0,0)). For '+' this happens to be correct, but for '*' it is wrong: every thread starts from (0,0), so the product collapses to (0,0) on the host -- even with a single thread, since this is purely a wrong identity element and not a parallel-combine problem.
Fix the BO_Mul case in actOnOMPReductionKindClause to also synthesize the integer-literal '1' initializer for C++ record types, mirroring the existing scalar/_Complex path. The converting constructor then builds the multiplicative identity (e.g. std::complex(1) == (1,0)). If the type is not constructible from '1', AddInitializerToDecl diagnoses it and the list item is dropped, so we never silently miscompile (no ICE). BO_Add is intentionally left relying on value-initialization for class types, because there the (0,0) default is the correct additive identity in the common case.
Adds a codegen test verifying the private copy is initialized to (1,0) for '*' and (0,0) for '+', a -verify test covering the diagnostic for a class type that is not constructible from '1', and a release note.
>From 38dba947bb84cde82364ba06fa2d7630502fff03 Mon Sep 17 00:00:00 2001
From: Dmitry Mikushin <dmitry at kernelgen.org>
Date: Sun, 7 Jun 2026 00:39:55 +0200
Subject: [PATCH] [Clang][OpenMP] Fix '*' reduction identity for class types
Clang accepts OpenMP built-in reductions over C++ class types such as
std::complex<T> as an extension (the OpenMP standard only defines the
implicitly-declared reduction identities for arithmetic and, in Clang, for
_Complex types; GCC rejects the construct outright). For such class types the
private reduction copy was left value-initialized, which yields the *additive*
identity (e.g. std::complex(0,0)). For '+' this happens to be correct, but for
'*' it is wrong: every thread starts from (0,0), so the product collapses to
(0,0) on the host -- even with a single thread, since this is purely a wrong
identity element and not a parallel-combine problem.
Fix the BO_Mul case in actOnOMPReductionKindClause to also synthesize the
integer-literal '1' initializer for C++ record types, mirroring the existing
scalar/_Complex path. The converting constructor then builds the multiplicative
identity (e.g. std::complex(1) == (1,0)). If the type is not constructible from
'1', AddInitializerToDecl diagnoses it and the list item is dropped, so we never
silently miscompile (no ICE). BO_Add is intentionally left relying on
value-initialization for class types, because there the (0,0) default is the
correct additive identity in the common case.
Adds a codegen test verifying the private copy is initialized to (1,0) for '*'
and (0,0) for '+', a -verify test covering the diagnostic for a class type that
is not constructible from '1', and a release note.
---
clang/docs/ReleaseNotes.rst | 4 ++
clang/lib/Sema/SemaOpenMP.cpp | 21 ++++++++-
.../for_reduction_class_identity_codegen.cpp | 43 +++++++++++++++++++
.../for_reduction_class_identity_messages.cpp | 40 +++++++++++++++++
4 files changed, 107 insertions(+), 1 deletion(-)
create mode 100644 clang/test/OpenMP/for_reduction_class_identity_codegen.cpp
create mode 100644 clang/test/OpenMP/for_reduction_class_identity_messages.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index bf917b3f642bc..25a2e9e59e9b3 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -969,6 +969,10 @@ OpenMP Support
``fallback`` modifier (``fb_nullify`` or ``fb_preserve``) with OpenMP >= 61.
- Added support for ``local`` clause with declare_target directive when
OpenMP >= 60.
+- Fixed the identity element used for ``reduction(* : x)`` over C++ class types
+ (e.g. ``std::complex``). The private copy is now initialized to the
+ multiplicative identity instead of being value-initialized, which previously
+ produced a wrong result (the product collapsed to the additive identity).
SYCL Support
------------
diff --git a/clang/lib/Sema/SemaOpenMP.cpp b/clang/lib/Sema/SemaOpenMP.cpp
index 76b40a5039180..1d11776824976 100644
--- a/clang/lib/Sema/SemaOpenMP.cpp
+++ b/clang/lib/Sema/SemaOpenMP.cpp
@@ -20911,9 +20911,28 @@ static bool actOnOMPReductionKindClause(
Init = S.ActOnIntegerConstant(ELoc, /*Val=*/0).get();
break;
case BO_Mul:
+ // '*' reduction op - initializer is '1'.
+ // For C++ class types (e.g. std::complex) the OpenMP built-in
+ // reduction identifiers are an extension: the standard only defines
+ // identities for arithmetic (and, in Clang, _Complex) types. Without
+ // an explicit initializer the private copy would be value-initialized,
+ // which yields the *additive* identity (e.g. std::complex(0,0)) and is
+ // wrong for multiplication. Initialize from the integer literal '1'
+ // instead and let the converting constructor build the multiplicative
+ // identity (e.g. std::complex(1) == (1,0)). If the type is not
+ // constructible from '1', AddInitializerToDecl below diagnoses it and
+ // the list item is dropped, so we never silently miscompile.
+ // Note: BO_Add deliberately keeps relying on value-initialization for
+ // class types, because there the (0,0) default is the correct additive
+ // identity for the common case; only '*' needs this special handling.
+ if (Type->isScalarType() || Type->isAnyComplexType() ||
+ (S.getLangOpts().CPlusPlus && Type->isRecordType())) {
+ Init = S.ActOnIntegerConstant(ELoc, /*Val=*/1).get();
+ }
+ break;
case BO_LAnd:
if (Type->isScalarType() || Type->isAnyComplexType()) {
- // '*' and '&&' reduction ops - initializer is '1'.
+ // '&&' reduction ops - initializer is '1'.
Init = S.ActOnIntegerConstant(ELoc, /*Val=*/1).get();
}
break;
diff --git a/clang/test/OpenMP/for_reduction_class_identity_codegen.cpp b/clang/test/OpenMP/for_reduction_class_identity_codegen.cpp
new file mode 100644
index 0000000000000..34640f51b31b2
--- /dev/null
+++ b/clang/test/OpenMP/for_reduction_class_identity_codegen.cpp
@@ -0,0 +1,43 @@
+// RUN: %clang_cc1 -verify -fopenmp -x c++ -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s
+// expected-no-diagnostics
+
+struct Complex {
+ double Re;
+ double Im;
+ Complex(double Re = 0.0, double Im = 0.0) : Re(Re), Im(Im) {}
+ Complex &operator+=(const Complex &RHS) {
+ Re += RHS.Re;
+ Im += RHS.Im;
+ return *this;
+ }
+ Complex &operator*=(const Complex &RHS) {
+ double NewRe = Re * RHS.Re - Im * RHS.Im;
+ Im = Re * RHS.Im + Im * RHS.Re;
+ Re = NewRe;
+ return *this;
+ }
+};
+
+void add_reduction(Complex *Data) {
+ Complex Sum;
+#pragma omp parallel for reduction(+ : Sum)
+ for (int I = 0; I < 4; ++I)
+ Sum += Data[I];
+}
+
+void mul_reduction(Complex *Data) {
+ Complex Product;
+#pragma omp parallel for reduction(* : Product)
+ for (int I = 0; I < 4; ++I)
+ Product *= Data[I];
+}
+
+// CHECK-LABEL: define {{.*}}void @_Z13add_reductionP7Complex
+// CHECK: call {{.*}} @_ZN7ComplexC1Edd(ptr {{[^,]*}}%{{[^,]*}}, double {{.*}}0.000000e+00, double {{.*}}0.000000e+00)
+// CHECK-LABEL: define internal {{.*}}void @_Z13add_reductionP7Complex.omp_outlined
+// CHECK: call {{.*}} @_ZN7ComplexC1Edd(ptr {{[^,]*}}%{{[^,]*}}, double {{.*}}0.000000e+00, double {{.*}}0.000000e+00)
+
+// CHECK-LABEL: define {{.*}}void @_Z13mul_reductionP7Complex
+// CHECK: call {{.*}} @_ZN7ComplexC1Edd(ptr {{[^,]*}}%{{[^,]*}}, double {{.*}}0.000000e+00, double {{.*}}0.000000e+00)
+// CHECK-LABEL: define internal {{.*}}void @_Z13mul_reductionP7Complex.omp_outlined
+// CHECK: call {{.*}} @_ZN7ComplexC1Edd(ptr {{[^,]*}}%{{[^,]*}}, double {{.*}}1.000000e+00, double {{.*}}0.000000e+00)
diff --git a/clang/test/OpenMP/for_reduction_class_identity_messages.cpp b/clang/test/OpenMP/for_reduction_class_identity_messages.cpp
new file mode 100644
index 0000000000000..7cf75ad641d93
--- /dev/null
+++ b/clang/test/OpenMP/for_reduction_class_identity_messages.cpp
@@ -0,0 +1,40 @@
+// RUN: %clang_cc1 -verify -fopenmp -x c++ -triple x86_64-unknown-linux-gnu -ferror-limit 100 %s
+// RUN: %clang_cc1 -verify -fopenmp-simd -x c++ -triple x86_64-unknown-linux-gnu -ferror-limit 100 %s
+
+// Multiplication reduction over a class type that *is* constructible from the
+// integer literal '1' (e.g. std::complex-like) is accepted: the multiplicative
+// identity is built via the converting constructor. See
+// for_reduction_class_identity_codegen.cpp for the value check.
+struct WithInt {
+ double V;
+ WithInt(int X = 1) : V(X) {}
+ WithInt &operator*=(const WithInt &RHS) {
+ V *= RHS.V;
+ return *this;
+ }
+};
+
+void ok(WithInt *Data) {
+ WithInt Product;
+#pragma omp parallel for reduction(* : Product)
+ for (int I = 0; I < 4; ++I)
+ Product *= Data[I];
+}
+
+// Multiplication reduction over a class type that is NOT constructible from '1'
+// must be diagnosed rather than silently miscompiled (it previously fell back
+// to value-initialization, producing the wrong (additive) identity).
+struct NoInt { // expected-note 2 {{candidate constructor}}
+ double V;
+ NoInt &operator*=(const NoInt &RHS) {
+ V *= RHS.V;
+ return *this;
+ }
+};
+
+void bad(NoInt *Data) {
+ NoInt Product;
+#pragma omp parallel for reduction(* : Product) // expected-error {{no viable conversion from 'int' to 'NoInt'}}
+ for (int I = 0; I < 4; ++I)
+ Product *= Data[I];
+}
More information about the cfe-commits
mailing list