Add absl::strings_internal::AbslStringifyStream

PiperOrigin-RevId: 883300728
Change-Id: Ib5c83114fdc4e292ad8fcf96a899878315707353
diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake
index 164b213..1fa6afb 100644
--- a/CMake/AbseilDll.cmake
+++ b/CMake/AbseilDll.cmake
@@ -367,6 +367,7 @@
   "strings/internal/string_constant.h"
   "strings/internal/stringify_sink.cc"
   "strings/internal/stringify_sink.h"
+  "strings/internal/stringify_stream.h"
   "strings/internal/utf8.cc"
   "strings/internal/utf8.h"
   "strings/match.cc"
diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel
index 4e766fd..6762ecd 100644
--- a/absl/strings/BUILD.bazel
+++ b/absl/strings/BUILD.bazel
@@ -424,6 +424,35 @@
     ],
 )
 
+cc_library(
+    name = "stringify_stream",
+    hdrs = ["internal/stringify_stream.h"],
+    copts = ABSL_DEFAULT_COPTS,
+    linkopts = ABSL_DEFAULT_LINKOPTS,
+    visibility = [
+        "//visibility:private",
+    ],
+    deps = [
+        ":string_view",
+        "//absl/base:config",
+    ],
+)
+
+cc_test(
+    name = "stringify_stream_test",
+    srcs = ["internal/stringify_stream_test.cc"],
+    copts = ABSL_TEST_COPTS,
+    linkopts = ABSL_DEFAULT_LINKOPTS,
+    deps = [
+        ":string_view",
+        ":stringify_stream",
+        "//absl/base:config",
+        "//absl/strings:str_format",
+        "@googletest//:gtest",
+        "@googletest//:gtest_main",
+    ],
+)
+
 cc_binary(
     name = "charset_benchmark",
     testonly = True,
diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt
index 520755b..50c6b11 100644
--- a/absl/strings/CMakeLists.txt
+++ b/absl/strings/CMakeLists.txt
@@ -118,6 +118,32 @@
   PUBLIC
 )
 
+absl_cc_library(
+  NAME
+    stringify_stream
+  HDRS
+    "internal/stringify_stream.h"
+  COPTS
+    ${ABSL_DEFAULT_COPTS}
+  DEPS
+    absl::string_view
+    absl::config
+)
+
+absl_cc_test(
+  NAME
+    stringify_stream_test
+  SRCS
+    "internal/stringify_stream_test.cc"
+  COPTS
+    ${ABSL_TEST_COPTS}
+  DEPS
+    absl::string_view
+    absl::stringify_stream
+    absl::str_format
+    GTest::gmock_main
+)
+
 # Internal-only target, do not depend on directly.
 absl_cc_library(
   NAME
diff --git a/absl/strings/internal/stringify_stream.h b/absl/strings/internal/stringify_stream.h
new file mode 100644
index 0000000..ca0e07a
--- /dev/null
+++ b/absl/strings/internal/stringify_stream.h
@@ -0,0 +1,112 @@
+// Copyright 2026 The Abseil Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// -----------------------------------------------------------------------------
+// File: stringify_stream.h
+// -----------------------------------------------------------------------------
+
+#ifndef ABSL_STRINGS_INTERNAL_STRINGIFY_STREAM_H_
+#define ABSL_STRINGS_INTERNAL_STRINGIFY_STREAM_H_
+
+// StringifyStream is an adaptor for any std::ostream, that provides a
+// stream insertion (<<) operator with the following behavior when inserting
+// some value of type T:
+//
+//   - If there is a suitable overload of operator<< already defined for T, it
+//     will be used.
+//
+//   - If there is no operator<< overload, but there is an AbslStringify defined
+//     for T, it will be used as a fallback.
+//
+//   - Otherwise it is a compilation error.
+//
+// For reference, AbslStringify typically has the form:
+//
+//   struct Foo {
+//     template <typename Sink>
+//     friend void AbslStringify(Sink& sink, const Foo& foo) { ... }
+//   };
+//
+// This permits the following usage, for example:
+//
+//   StringifyStream(std::cout) << Foo();
+//
+
+#include <cstddef>
+#include <ostream>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+#include "absl/base/config.h"
+#include "absl/strings/string_view.h"
+
+namespace absl {
+ABSL_NAMESPACE_BEGIN
+
+namespace strings_internal {
+
+class StringifyStream {
+ public:
+  // Constructor: adapts (but does not take ownership of) some underlying
+  // std::ostream instance.
+  explicit StringifyStream(std::ostream& os) : sink_{os} {}
+
+  // Stream insertion: delegate to an overload of operator<< if defined for type
+  // T, otherwise fall back to AbslStringify for T.
+  template <typename T>
+  friend StringifyStream& operator<<(StringifyStream& stream, const T& t) {
+    if constexpr (HasStreamInsertion<T>::value) {
+      stream.sink_.os << t;
+    } else {
+      AbslStringify(stream.sink_, t);
+    }
+    return stream;
+  }
+
+  // Rvalue-ref overload, required when the StringifyStream parameter hasn't
+  // been bound to a variable.
+  template <typename T>
+  friend StringifyStream& operator<<(StringifyStream&& stream, const T& t) {
+    return stream << t;
+  }
+
+ private:
+  // Abseil "stringify sink" concept (stringify_sink.h)
+  struct Sink {
+    std::ostream& os;
+    void Append(size_t count, char ch) { os << std::string(count, ch); }
+    void Append(absl::string_view v) { os << v; }
+    friend void AbslFormatFlush(Sink* sink, absl::string_view v) {
+      sink->Append(v);
+    }
+  } sink_;
+
+  // SFINAE helper to identify types with a defined operator<< overload.
+  template <typename T, typename = void>
+  struct HasStreamInsertion : std::false_type {};
+
+  template <typename T>
+  struct HasStreamInsertion<T,
+                            std::void_t<decltype(std::declval<std::ostream&>()
+                                                 << std::declval<const T&>())>>
+      : std::true_type {};
+};
+
+}  // namespace strings_internal
+
+ABSL_NAMESPACE_END
+}  // namespace absl
+
+#endif  // ABSL_STRINGS_INTERNAL_STRINGIFY_STREAM_H_
diff --git a/absl/strings/internal/stringify_stream_test.cc b/absl/strings/internal/stringify_stream_test.cc
new file mode 100644
index 0000000..8e336a1
--- /dev/null
+++ b/absl/strings/internal/stringify_stream_test.cc
@@ -0,0 +1,100 @@
+// Copyright 2026 The Abseil Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "absl/strings/internal/stringify_stream.h"
+
+#include <cstddef>
+#include <ostream>
+#include <sstream>
+
+#include "gtest/gtest.h"
+#include "absl/base/config.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/string_view.h"
+
+namespace absl {
+ABSL_NAMESPACE_BEGIN
+namespace strings_internal {
+namespace {
+
+// Exercises the Append(size_t, char) overload
+struct AppendNCharsTest {
+  size_t count;
+  char ch;
+
+  template <typename Sink>
+  friend void AbslStringify(Sink& sink, const AppendNCharsTest& t) {
+    sink.Append(t.count, t.ch);
+  }
+};
+TEST(StringifyStreamTest, AppendNChars) {
+  std::ostringstream os;
+  StringifyStream(os) << AppendNCharsTest{5, 'a'};
+  EXPECT_EQ(os.str(), "aaaaa");
+}
+
+// Exercises the Append(absl::string_view) overload
+struct AppendStringViewTest {
+  absl::string_view v;
+
+  template <typename Sink>
+  friend void AbslStringify(Sink& sink, const AppendStringViewTest& t) {
+    sink.Append(t.v);
+  }
+};
+TEST(StringifyStreamTest, AppendStringView) {
+  std::ostringstream os;
+  StringifyStream(os) << AppendStringViewTest{"abc"};
+  EXPECT_EQ(os.str(), "abc");
+}
+
+// Exercises AbslFormatFlush(OStringStreamSink*, absl::string_view v)
+struct AbslFormatFlushTest {
+  absl::string_view a, b, c;
+
+  template <typename Sink>
+  friend void AbslStringify(Sink& sink, const AbslFormatFlushTest& t) {
+    absl::Format(&sink, "%s, %s, %s", t.a, t.b, t.c);
+  }
+};
+TEST(StringifyStreamTest, AbslFormatFlush) {
+  std::ostringstream os;
+  StringifyStream(os) << AbslFormatFlushTest{"a", "b", "c"};
+  EXPECT_EQ(os.str(), "a, b, c");
+}
+
+// If overloads of both AbslStringify and operator<< are defined for the type,
+// the operator<< overload should take precedence.
+struct PreferStreamInsertionOverAbslStringifyTest {
+  friend std::ostream& operator<<(  // NOLINT(clang-diagnostic-unused-function)
+      std::ostream& os, const PreferStreamInsertionOverAbslStringifyTest&) {
+    return os << "good";
+  }
+
+  template <typename Sink>
+  friend void AbslStringify  // NOLINT(clang-diagnostic-unused-function)
+      (Sink& sink, const PreferStreamInsertionOverAbslStringifyTest&) {
+    sink.Append("bad");
+  }
+};
+TEST(StringifyStreamTest, PreferStreamInsertionOverAbslStringify) {
+  std::ostringstream os;
+  StringifyStream(os) << PreferStreamInsertionOverAbslStringifyTest{};
+  EXPECT_EQ(os.str(), "good");
+}
+
+}  // namespace
+}  // namespace strings_internal
+ABSL_NAMESPACE_END
+}  // namespace absl