[libcxx-commits] [libcxx] [libc++][ranges] LWG4121: `ranges::to` constructs associative containers via `c.emplace(c.end(), *it)` (PR #220481)

via libcxx-commits libcxx-commits at lists.llvm.org
Tue Sep 1 22:01:57 PDT 2026


https://github.com/Nawazz-ish created https://github.com/llvm/llvm-project/pull/220481

Implements the resolution of LWG4121.

`ranges::to`'s append path probed for `c.emplace(c.end(), *it)`. Associative containers forward every argument of `emplace()` to the element constructor instead of treating the first one as a position hint, so that probe does not mean what the wording intended. The resolution spells the hinted form `emplace_hint`.

Both the exposition-only *container-appendable* concept and the append branch are updated. The test container is renamed to match, since after this change there is no `emplace(pos, val)` branch left to cover. `EmplaceOrInsert` exercises the behavioural change directly: a container offering only the unhinted `emplace` now has to be appended to via `insert`.

Fixes #204441


>From 65ab28c9bc0128581a5556849bc39ab8bda13812 Mon Sep 17 00:00:00 2001
From: Nawaz <enzishh at icloud.com>
Date: Wed, 19 Aug 2026 08:32:59 +0530
Subject: [PATCH] [libc++][ranges] LWG4121: ranges::to constructs associative
 containers via c.emplace(c.end(), *it)

Associative containers forward the arguments of emplace() to the element
constructor rather than treating the first one as a position hint, so
c.emplace(c.end(), *it) does not do what the append path intended. Use
emplace_hint(c.end(), *it) instead, in both the container-appendable concept
and the append branch.

The test container is renamed to match. After this change there is no
emplace(pos, val) branch left, so InserterChoice::Emplace becomes EmplaceHint
rather than gaining a new value alongside it.

Renaming that container shows the new member is called, but not that the old
one is no longer selected. test_lwg_4121 covers the second half: a container
offering only the unhinted emplace(pos, val) alongside insert must now be
appended to via insert. It is kept out of the InserterChoice ladder, which is
a cumulative rank, so that adding it does not hand every EmplaceHint container
an emplace() and weaken the existing priority test.
---
 libcxx/docs/Status/Cxx29Issues.csv            |  2 +-
 libcxx/include/__ranges/to.h                  |  6 +-
 .../range.utility.conv/container.h            |  8 +--
 .../range.utility.conv/to.pass.cpp            | 56 ++++++++++++++++++-
 4 files changed, 62 insertions(+), 10 deletions(-)

diff --git a/libcxx/docs/Status/Cxx29Issues.csv b/libcxx/docs/Status/Cxx29Issues.csv
index d84eeb44a23ff..410b318344d4c 100644
--- a/libcxx/docs/Status/Cxx29Issues.csv
+++ b/libcxx/docs/Status/Cxx29Issues.csv
@@ -1,6 +1,6 @@
 "Issue #","Issue Name","Meeting","Status","First released version","GitHub issue","Notes"
 "`LWG3417 <https://wg21.link/LWG3417>`__","Missing ``volatile`` atomic deprecations","2026-06 (Brno)","","","`#204440 <https://github.com/llvm/llvm-project/issues/204440>`__",""
-"`LWG4121 <https://wg21.link/LWG4121>`__","``ranges::to`` constructs associative containers via ``c.emplace(c.end(), *it)``","2026-06 (Brno)","","","`#204441 <https://github.com/llvm/llvm-project/issues/204441>`__",""
+"`LWG4121 <https://wg21.link/LWG4121>`__","``ranges::to`` constructs associative containers via ``c.emplace(c.end(), *it)``","2026-06 (Brno)","|Complete|","24","`#204441 <https://github.com/llvm/llvm-project/issues/204441>`__",""
 "`LWG4125 <https://wg21.link/LWG4125>`__","``move_iterator``'s default constructor should be constrained","2026-06 (Brno)","|Complete|","23","`#204442 <https://github.com/llvm/llvm-project/issues/204442>`__",""
 "`LWG4158 <https://wg21.link/LWG4158>`__","``packaged_task::operator=`` should abandon its shared state","2026-06 (Brno)","","","`#204443 <https://github.com/llvm/llvm-project/issues/204443>`__",""
 "`LWG4182 <https://wg21.link/LWG4182>`__","Definition of ``NULL`` is too broad","2026-06 (Brno)","","","`#204444 <https://github.com/llvm/llvm-project/issues/204444>`__",""
diff --git a/libcxx/include/__ranges/to.h b/libcxx/include/__ranges/to.h
index 3a2bc42969f7b..3734916530eae 100644
--- a/libcxx/include/__ranges/to.h
+++ b/libcxx/include/__ranges/to.h
@@ -57,7 +57,7 @@ constexpr bool __container_appendable = requires(_Container& __c, _Ref&& __ref)
   requires(
       requires { __c.emplace_back(std::forward<_Ref>(__ref)); } ||
       requires { __c.push_back(std::forward<_Ref>(__ref)); } ||
-      requires { __c.emplace(__c.end(), std::forward<_Ref>(__ref)); } ||
+      requires { __c.emplace_hint(__c.end(), std::forward<_Ref>(__ref)); } ||
       requires { __c.insert(__c.end(), std::forward<_Ref>(__ref)); });
 };
 
@@ -117,8 +117,8 @@ template <class _Container, input_range _Range, class... _Args>
           __result.emplace_back(std::forward<_Ref>(__ref));
         } else if constexpr (requires { __result.push_back(std::declval<_Ref>()); }) {
           __result.push_back(std::forward<_Ref>(__ref));
-        } else if constexpr (requires { __result.emplace(__result.end(), std::declval<_Ref>()); }) {
-          __result.emplace(__result.end(), std::forward<_Ref>(__ref));
+        } else if constexpr (requires { __result.emplace_hint(__result.end(), std::declval<_Ref>()); }) {
+          __result.emplace_hint(__result.end(), std::forward<_Ref>(__ref));
         } else {
           static_assert(requires { __result.insert(__result.end(), std::declval<_Ref>()); });
           __result.insert(__result.end(), std::forward<_Ref>(__ref));
diff --git a/libcxx/test/std/ranges/range.utility/range.utility.conv/container.h b/libcxx/test/std/ranges/range.utility/range.utility.conv/container.h
index 4fd52680add9b..b7927fcf2ab8d 100644
--- a/libcxx/test/std/ranges/range.utility/range.utility.conv/container.h
+++ b/libcxx/test/std/ranges/range.utility/range.utility.conv/container.h
@@ -14,7 +14,7 @@
 
 enum class CtrChoice { Invalid, DefaultCtrAndInsert, BeginEndPair, FromRangeT, DirectCtr };
 
-enum class InserterChoice { Invalid, Insert, Emplace, PushBack, EmplaceBack };
+enum class InserterChoice { Invalid, Insert, EmplaceHint, PushBack, EmplaceBack };
 
 // Allows checking that `ranges::to` correctly follows the order of priority of different constructors -- e.g., if
 // 3 constructors are available, the `from_range_t` constructor is chosen in favor of the constructor taking two
@@ -118,10 +118,10 @@ struct Container {
   }
 
   template <class T>
-  constexpr ElementType* emplace(ElementType* where, T val)
-    requires(Inserter >= InserterChoice::Emplace)
+  constexpr ElementType* emplace_hint(ElementType* where, T val)
+    requires(Inserter >= InserterChoice::EmplaceHint)
   {
-    inserter_choice = InserterChoice::Emplace;
+    inserter_choice = InserterChoice::EmplaceHint;
     return __insert_impl(where, val);
   }
 
diff --git a/libcxx/test/std/ranges/range.utility/range.utility.conv/to.pass.cpp b/libcxx/test/std/ranges/range.utility/range.utility.conv/to.pass.cpp
index a983745fd636e..8d9e0713450a7 100644
--- a/libcxx/test/std/ranges/range.utility/range.utility.conv/to.pass.cpp
+++ b/libcxx/test/std/ranges/range.utility/range.utility.conv/to.pass.cpp
@@ -435,8 +435,8 @@ constexpr void test_ctr_choice_order() {
 
     case_4.operator()<InserterChoice::Insert, false>();
     case_4.operator()<InserterChoice::Insert, true>();
-    case_4.operator()<InserterChoice::Emplace, false>();
-    case_4.operator()<InserterChoice::Emplace, true>();
+    case_4.operator()<InserterChoice::EmplaceHint, false>();
+    case_4.operator()<InserterChoice::EmplaceHint, true>();
     case_4.operator()<InserterChoice::PushBack, false>();
     case_4.operator()<InserterChoice::PushBack, true>();
     case_4.operator()<InserterChoice::EmplaceBack, false>();
@@ -496,6 +496,57 @@ constexpr void test_lwg_3785() {
   }
 }
 
+// A container that spells the hinted insert as the unhinted `emplace`. Before LWG 4121,
+// `ranges::to` probed for `c.emplace(c.end(), *it)` and would call this, passing the end iterator
+// as the first constructor argument. Associative containers spell the hinted form `emplace_hint`,
+// so this overload must no longer be selected and appending has to fall through to `insert`.
+struct EmplaceOrInsert {
+  using value_type = int;
+
+  static constexpr int Capacity = 8;
+  int buffer_[Capacity]         = {};
+  int size_                     = 0;
+  bool called_emplace           = false;
+  bool called_insert            = false;
+
+  constexpr int* begin() { return buffer_; }
+  constexpr int* end() { return buffer_ + size_; }
+  constexpr int size() const { return size_; }
+
+  constexpr int* emplace(int* where, int val) {
+    called_emplace = true;
+    return __insert_impl(where, val);
+  }
+
+  constexpr int* insert(int* where, int val) {
+    called_insert = true;
+    return __insert_impl(where, val);
+  }
+
+  constexpr int* __insert_impl(int* where, int val) {
+    assert(size() + 1 <= Capacity);
+    std::shift_right(where, end(), 1);
+    *where = val;
+    ++size_;
+    return where;
+  }
+};
+
+constexpr void test_lwg_4121() {
+  // Test LWG 4121 ("`ranges::to` constructs associative containers via `c.emplace(c.end(), *it)`").
+  // `emplace` on an associative container forwards every argument to the element constructor rather
+  // than treating the first as a position hint, so the hinted form must be spelled `emplace_hint`.
+  // A container offering only the unhinted `emplace` has to be appended to via `insert`.
+  std::array in = {1, 2, 3};
+
+  std::same_as<EmplaceOrInsert> decltype(auto) result = std::ranges::to<EmplaceOrInsert>(in);
+
+  assert(!result.called_emplace);
+  assert(result.called_insert);
+  assert(result.size() == 3);
+  assert(std::ranges::equal(result, in));
+}
+
 constexpr void test_recursive() {
   using C1 = Container<int, CtrChoice::DirectCtr>;
   using C2 = Container<C1, CtrChoice::FromRangeT>;
@@ -554,6 +605,7 @@ constexpr bool test() {
   test_constraints();
   test_ctr_choice_order();
   test_lwg_3785();
+  test_lwg_4121();
   test_recursive();
 
   return true;



More information about the libcxx-commits mailing list