[libcxx-commits] [libcxx] [llvm] [libc++] Fix optional<T>::transform to optional<T&> (GH#220332) (PR #220500)

via libcxx-commits libcxx-commits at lists.llvm.org
Wed Sep 2 00:20:21 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-backend-x86

Author: Yuvraj Choudhary (yuvrajmnit007)

<details>
<summary>Changes</summary>

Fixes https://github.com/llvm/llvm-project/issues/220332

## Problem

`std::optional<T>::transform(f)` fails to compile when `f` returns a
reference type, e.g.:

```cpp
struct foo {
    int& member() { return i_; }
    int i_ = 0;
};

std::optional<foo> opt(std::in_place);
auto mem = opt.transform(&foo::member); // error: calling a private constructor

---
Full diff: https://github.com/llvm/llvm-project/pull/220500.diff


4 Files Affected:

- (modified) libcxx/include/__optional/optional_ref.h (+3) 
- (modified) libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp (+12) 
- (modified) llvm/lib/Target/TargetLoweringObjectFile.cpp (+32) 
- (added) llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll (+71) 


``````````diff
diff --git a/libcxx/include/__optional/optional_ref.h b/libcxx/include/__optional/optional_ref.h
index 20270e0594df5..0ed3869c2136e 100644
--- a/libcxx/include/__optional/optional_ref.h
+++ b/libcxx/include/__optional/optional_ref.h
@@ -79,6 +79,9 @@ class optional<_Tp&> : public __optional_ref_iterator_base<_Tp&> {
 private:
   _Tp* __value_ = nullptr;
 
+  template <class>
+  friend class optional;
+
   template <class _Up>
   _LIBCPP_HIDE_FROM_ABI constexpr void __convert_init_ref_val(_Up&& __val) {
     _Tp& __r(std::forward<_Up>(__val));
diff --git a/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp b/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
index 96303c5796f6d..9a0ab6488ca16 100644
--- a/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
@@ -321,6 +321,18 @@ constexpr bool test_ref() {
     assert(&(*o2) == &j);
   }
 
+  // https://github.com/llvm/llvm-project/issues/220332
+  {
+    struct foo {
+      int& member() { return i_; }
+      int i_ = 0;
+    };
+    std::optional<foo> opt(std::in_place);
+    std::same_as<std::optional<int&>> decltype(auto) mem = opt.transform(&foo::member);
+    assert(mem.has_value());
+    assert(&(*mem) == &opt->i_);
+  }
+
   return true;
 }
 #endif
diff --git a/llvm/lib/Target/TargetLoweringObjectFile.cpp b/llvm/lib/Target/TargetLoweringObjectFile.cpp
index b94c374e1dbd5..8562ef04e0550 100644
--- a/llvm/lib/Target/TargetLoweringObjectFile.cpp
+++ b/llvm/lib/Target/TargetLoweringObjectFile.cpp
@@ -80,6 +80,26 @@ static bool isNullOrUndef(const Constant *C) {
   return true;
 }
 
+/// Return true if the constant contains any undef or poison sub-elements,
+/// recursively traversing aggregate types. PoisonValue is a subclass of
+/// UndefValue, so isa<UndefValue> catches both.
+///
+/// Such constants must not be placed in mergeable sections: the ELF linker
+/// merges constants in those sections by comparing byte content, but undef/
+/// poison bytes are emitted as zeros, making them byte-identical to a
+/// zeroinitializer constant. Allowing this merge would violate unnamed_addr
+/// semantics. See https://github.com/llvm/llvm-project/issues/220091.
+static bool containsUndefOrPoison(const Constant *C) {
+  if (isa<UndefValue>(C))
+    return true;
+  if (!isa<ConstantAggregate>(C))
+    return false;
+  for (const auto *Operand : C->operand_values())
+    if (containsUndefOrPoison(cast<Constant>(Operand)))
+      return true;
+  return false;
+}
+
 static bool isSuitableForBSS(const GlobalVariable *GV) {
   const Constant *C = GV->getInitializer();
 
@@ -316,6 +336,18 @@ SectionKind TargetLoweringObjectFile::getKindForGlobal(const GlobalObject *GO,
       if (!GVar->hasGlobalUnnamedAddr())
         return SectionKind::getReadOnly();
 
+      // Do not place constants whose initializer contains undef or poison
+      // sub-elements into mergeable sections. The ELF linker deduplicates
+      // constants in such sections by comparing raw byte content, but undef
+      // and poison bytes are both emitted as zeros. This would make a constant
+      // like `<{ [4 x i8] undef, [4 x i8] zeroinitializer }>` byte-identical
+      // to `zeroinitializer`, allowing the linker to merge them to the same
+      // address even though they are not semantically equivalent. Placing the
+      // constant in non-mergeable .rodata prevents this incorrect merge.
+      // See https://github.com/llvm/llvm-project/issues/220091.
+      if (containsUndefOrPoison(C))
+        return SectionKind::getReadOnly();
+
       // If initializer is a null-terminated string, put it in a "cstring"
       // section of the right width.
       if (ArrayType *ATy = dyn_cast<ArrayType>(C->getType())) {
diff --git a/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll b/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll
new file mode 100644
index 0000000000000..e6a2576fdd56d
--- /dev/null
+++ b/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll
@@ -0,0 +1,71 @@
+; RUN: llc < %s -mtriple=x86_64-unknown-linux-gnu | FileCheck %s
+;
+; Regression test for https://github.com/llvm/llvm-project/issues/220091
+;
+; unnamed_addr constants that contain undef or poison sub-elements must NOT
+; be placed in ELF mergeable sections (.rodata.cstN, SHF_MERGE "aM" flag).
+;
+; The ELF linker merges constants in those sections by comparing raw byte
+; content. LLVM emits undef and poison bytes as zeros, which makes such a
+; constant byte-identical to a zeroinitializer constant of the same size.
+; The linker would then assign them the same address even though they are
+; semantically distinct under the LLVM IR spec, leading to miscompilation.
+;
+; Fix: getKindForGlobal() now returns ReadOnly (plain .rodata) instead of a
+; MergeableConst* section kind when the initializer contains undef or poison.
+
+; ---------------------------------------------------------------------------
+; @undef_and_zero: struct with undef first half, zero second half (8 bytes).
+; Even though it's 8 bytes and unnamed_addr, it must NOT go to .rodata.cst8
+; because the undef half would be emitted as zeros, making it byte-identical
+; to @all_zero below.
+; ---------------------------------------------------------------------------
+ at undef_and_zero = private unnamed_addr constant <{ [4 x i8], [4 x i8] }> <{ [4 x i8] undef, [4 x i8] zeroinitializer }>, align 4
+
+; CHECK:     .type	.Lundef_and_zero, at object
+; CHECK-NOT: .section	.rodata.cst8
+; CHECK:     .section	.rodata,"a", at progbits
+
+; ---------------------------------------------------------------------------
+; @all_zero: pure zeroinitializer of the same 8-byte size.
+; This SHOULD still go to .rodata.cst8 (no regression for non-undef case).
+; ---------------------------------------------------------------------------
+ at all_zero = private unnamed_addr constant i64 0, align 8
+
+; CHECK:     .type	.Lall_zero, at object
+; CHECK:     .section	.rodata.cst8,"aM", at progbits,8
+
+; ---------------------------------------------------------------------------
+; @nested_undef: undef nested inside a struct field.
+; Must be non-mergeable even though it's an aggregate with a concrete field.
+; ---------------------------------------------------------------------------
+ at nested_undef = private unnamed_addr constant { i32, i32 } { i32 undef, i32 42 }, align 4
+
+; CHECK:     .type	.Lnested_undef, at object
+; CHECK:     .section	.rodata,"a", at progbits
+
+; ---------------------------------------------------------------------------
+; @poison_field: poison value (PoisonValue is a subclass of UndefValue).
+; Must also be non-mergeable.
+; ---------------------------------------------------------------------------
+ at poison_field = private unnamed_addr constant { i32, i32 } { i32 poison, i32 0 }, align 4
+
+; CHECK:     .type	.Lpoison_field, at object
+; (stays in .rodata from the previous section, no new .section directive needed)
+
+; ---------------------------------------------------------------------------
+; @no_undef_4: fully concrete constant -- still gets mergeable section.
+; ---------------------------------------------------------------------------
+ at no_undef_4 = private unnamed_addr constant i32 1234, align 4
+
+; CHECK:     .type	.Lno_undef_4, at object
+; CHECK:     .section	.rodata.cst4,"aM", at progbits,4
+
+; Dummy function to prevent globals from being dead-stripped.
+define ptr @use() {
+  %a = select i1 true,  ptr @undef_and_zero, ptr @all_zero
+  %b = select i1 false, ptr @nested_undef,   ptr @poison_field
+  %c = select i1 true,  ptr %a,              ptr @no_undef_4
+  %d = select i1 false, ptr %b,              ptr %c
+  ret ptr %d
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/220500


More information about the libcxx-commits mailing list