From d9d4f0d5ef0b52dd1822fe9ae4ff13ce1261c4a9 Mon Sep 17 00:00:00 2001
From: Lukas Tenbrink <lukas.tenbrink@gmail.com>
Date: Wed, 11 Dec 2024 16:37:46 +0100
Subject: [PATCH] Add `String::concat` and `string.extend` functions, for
 string concatenations where the contributors to the final string are
 well-known at the time of call.

---
 core/math/aabb.cpp         |  5 +++-
 core/math/basis.cpp        |  8 +++--
 core/math/color.cpp        |  8 ++++-
 core/math/face3.cpp        |  5 +++-
 core/math/plane.cpp        |  5 +++-
 core/math/projection.cpp   | 10 ++++---
 core/math/quaternion.cpp   | 10 +++++--
 core/math/rect2.cpp        |  5 +++-
 core/math/rect2i.cpp       |  5 +++-
 core/math/transform_2d.cpp |  8 +++--
 core/math/transform_3d.cpp | 10 ++++---
 core/math/vector2.cpp      |  6 +++-
 core/math/vector2i.cpp     |  6 +++-
 core/math/vector3.cpp      |  7 ++++-
 core/math/vector3i.cpp     |  7 ++++-
 core/math/vector4.cpp      |  8 ++++-
 core/math/vector4i.cpp     |  8 ++++-
 core/string/ustring.h      | 61 ++++++++++++++++++++++++++++++++++++++
 core/variant/variant.cpp   |  4 +--
 19 files changed, 156 insertions(+), 30 deletions(-)

diff --git a/core/math/aabb.cpp b/core/math/aabb.cpp
index a1ad58cc63c9..4c8dc84936cb 100644
--- a/core/math/aabb.cpp
+++ b/core/math/aabb.cpp
@@ -449,5 +449,8 @@ Variant AABB::intersects_ray_bind(const Vector3 &p_from, const Vector3 &p_dir) c
 }
 
 AABB::operator String() const {
-	return "[P: " + position.operator String() + ", S: " + size + "]";
+	return String::concat(
+			"[P: ", position.operator String(),
+			", S: ", size,
+			"]");
 }
diff --git a/core/math/basis.cpp b/core/math/basis.cpp
index 4deb69f838db..f8837ad05e77 100644
--- a/core/math/basis.cpp
+++ b/core/math/basis.cpp
@@ -724,9 +724,11 @@ bool Basis::operator!=(const Basis &p_matrix) const {
 }
 
 Basis::operator String() const {
-	return "[X: " + get_column(0).operator String() +
-			", Y: " + get_column(1).operator String() +
-			", Z: " + get_column(2).operator String() + "]";
+	return String::concat(
+			"[X: ", get_column(0).operator String(),
+			", Y: ", get_column(1).operator String(),
+			", Z: ", get_column(2).operator String(),
+			"]");
 }
 
 Quaternion Basis::get_quaternion() const {
diff --git a/core/math/color.cpp b/core/math/color.cpp
index 94df2869196a..c735079189d2 100644
--- a/core/math/color.cpp
+++ b/core/math/color.cpp
@@ -487,7 +487,13 @@ Color Color::from_rgba8(int64_t p_r8, int64_t p_g8, int64_t p_b8, int64_t p_a8)
 }
 
 Color::operator String() const {
-	return "(" + String::num(r, 4) + ", " + String::num(g, 4) + ", " + String::num(b, 4) + ", " + String::num(a, 4) + ")";
+	return String::concat(
+			"(",
+			String::num(r, 4), ", ",
+			String::num(g, 4), ", ",
+			String::num(b, 4), ", ",
+			String::num(a, 4),
+			")");
 }
 
 Color Color::operator+(const Color &p_color) const {
diff --git a/core/math/face3.cpp b/core/math/face3.cpp
index 1dff0ee4a6f0..5e08a760ca7c 100644
--- a/core/math/face3.cpp
+++ b/core/math/face3.cpp
@@ -201,7 +201,10 @@ bool Face3::intersects_aabb(const AABB &p_aabb) const {
 }
 
 Face3::operator String() const {
-	return String() + vertex[0] + ", " + vertex[1] + ", " + vertex[2];
+	return String::concat(
+			vertex[0], ", ",
+			vertex[1], ", ",
+			vertex[2]);
 }
 
 void Face3::project_range(const Vector3 &p_normal, const Transform3D &p_transform, real_t &r_min, real_t &r_max) const {
diff --git a/core/math/plane.cpp b/core/math/plane.cpp
index 5422fa9ca816..e7ee248622e7 100644
--- a/core/math/plane.cpp
+++ b/core/math/plane.cpp
@@ -181,5 +181,8 @@ bool Plane::is_finite() const {
 }
 
 Plane::operator String() const {
-	return "[N: " + normal.operator String() + ", D: " + String::num_real(d, false) + "]";
+	return String::concat(
+			"[N: ", normal.operator String(),
+			", D: ", String::num_real(d, false),
+			"]");
 }
diff --git a/core/math/projection.cpp b/core/math/projection.cpp
index 39948e27eb58..c67c2d2483bc 100644
--- a/core/math/projection.cpp
+++ b/core/math/projection.cpp
@@ -869,10 +869,12 @@ void Projection::set_light_atlas_rect(const Rect2 &p_rect) {
 }
 
 Projection::operator String() const {
-	return "[X: " + columns[0].operator String() +
-			", Y: " + columns[1].operator String() +
-			", Z: " + columns[2].operator String() +
-			", W: " + columns[3].operator String() + "]";
+	return String::concat(
+			"[X: ", columns[0].operator String(),
+			", Y: ", columns[1].operator String(),
+			", Z: ", columns[2].operator String(),
+			", W: ", columns[3].operator String(),
+			"]");
 }
 
 real_t Projection::get_aspect() const {
diff --git a/core/math/quaternion.cpp b/core/math/quaternion.cpp
index de6802766475..d0aa5c56ff51 100644
--- a/core/math/quaternion.cpp
+++ b/core/math/quaternion.cpp
@@ -281,7 +281,13 @@ Quaternion Quaternion::spherical_cubic_interpolate_in_time(const Quaternion &p_b
 }
 
 Quaternion::operator String() const {
-	return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ", " + String::num_real(z, false) + ", " + String::num_real(w, false) + ")";
+	return String::concat(
+			"(",
+			String::num_real(x, false), ", ",
+			String::num_real(y, false), ", ",
+			String::num_real(z, false), ", ",
+			String::num_real(w, false),
+			")");
 }
 
 Vector3 Quaternion::get_axis() const {
@@ -298,7 +304,7 @@ real_t Quaternion::get_angle() const {
 
 Quaternion::Quaternion(const Vector3 &p_axis, real_t p_angle) {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_MSG(!p_axis.is_normalized(), "The axis Vector3 " + p_axis.operator String() + " must be normalized.");
+	ERR_FAIL_COND_MSG(!p_axis.is_normalized(), String::concat("The axis Vector3 ", p_axis.operator String(), " must be normalized."));
 #endif
 	real_t d = p_axis.length();
 	if (d == 0) {
diff --git a/core/math/rect2.cpp b/core/math/rect2.cpp
index 102a9e4b5410..76c1ea0b6acb 100644
--- a/core/math/rect2.cpp
+++ b/core/math/rect2.cpp
@@ -287,7 +287,10 @@ bool Rect2::intersects_transformed(const Transform2D &p_xform, const Rect2 &p_re
 }
 
 Rect2::operator String() const {
-	return "[P: " + position.operator String() + ", S: " + size.operator String() + "]";
+	return String::concat(
+			"[P: ", position.operator String(),
+			", S: ", size.operator String(),
+			"]");
 }
 
 Rect2::operator Rect2i() const {
diff --git a/core/math/rect2i.cpp b/core/math/rect2i.cpp
index 807fe0707a75..42a39a7ad558 100644
--- a/core/math/rect2i.cpp
+++ b/core/math/rect2i.cpp
@@ -34,7 +34,10 @@
 #include "core/string/ustring.h"
 
 Rect2i::operator String() const {
-	return "[P: " + position.operator String() + ", S: " + size + "]";
+	return String::concat(
+			"[P: ", position.operator String(),
+			", S: ", size,
+			"]");
 }
 
 Rect2i::operator Rect2() const {
diff --git a/core/math/transform_2d.cpp b/core/math/transform_2d.cpp
index c00d09ecd415..8bd08cf6d391 100644
--- a/core/math/transform_2d.cpp
+++ b/core/math/transform_2d.cpp
@@ -312,7 +312,9 @@ Transform2D Transform2D::operator/(real_t p_val) const {
 }
 
 Transform2D::operator String() const {
-	return "[X: " + columns[0].operator String() +
-			", Y: " + columns[1].operator String() +
-			", O: " + columns[2].operator String() + "]";
+	return String::concat(
+			"[X: ", columns[0].operator String(),
+			", Y: ", columns[1].operator String(),
+			", O: ", columns[2].operator String(),
+			"]");
 }
diff --git a/core/math/transform_3d.cpp b/core/math/transform_3d.cpp
index 654b83044508..da943d5f337f 100644
--- a/core/math/transform_3d.cpp
+++ b/core/math/transform_3d.cpp
@@ -223,10 +223,12 @@ Transform3D Transform3D::operator/(real_t p_val) const {
 }
 
 Transform3D::operator String() const {
-	return "[X: " + basis.get_column(0).operator String() +
-			", Y: " + basis.get_column(1).operator String() +
-			", Z: " + basis.get_column(2).operator String() +
-			", O: " + origin.operator String() + "]";
+	return String::concat(
+			"[X: ", basis.get_column(0).operator String(),
+			", Y: ", basis.get_column(1).operator String(),
+			", Z: ", basis.get_column(2).operator String(),
+			", O: ", origin.operator String(),
+			"]");
 }
 
 Transform3D::Transform3D(const Basis &p_basis, const Vector3 &p_origin) :
diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp
index 7a51c1394543..5eebd191e1a3 100644
--- a/core/math/vector2.cpp
+++ b/core/math/vector2.cpp
@@ -207,7 +207,11 @@ bool Vector2::is_finite() const {
 }
 
 Vector2::operator String() const {
-	return "(" + String::num_real(x, true) + ", " + String::num_real(y, true) + ")";
+	return String::concat(
+			"(",
+			String::num_real(x, true), ", ",
+			String::num_real(y, true),
+			")");
 }
 
 Vector2::operator Vector2i() const {
diff --git a/core/math/vector2i.cpp b/core/math/vector2i.cpp
index 790f56473492..7e903058a869 100644
--- a/core/math/vector2i.cpp
+++ b/core/math/vector2i.cpp
@@ -135,7 +135,11 @@ bool Vector2i::operator!=(const Vector2i &p_vec2) const {
 }
 
 Vector2i::operator String() const {
-	return "(" + itos(x) + ", " + itos(y) + ")";
+	return String::concat(
+			"(",
+			itos(x), ", ",
+			itos(y),
+			")");
 }
 
 Vector2i::operator Vector2() const {
diff --git a/core/math/vector3.cpp b/core/math/vector3.cpp
index 24422c574e85..6e09c2e2074a 100644
--- a/core/math/vector3.cpp
+++ b/core/math/vector3.cpp
@@ -169,7 +169,12 @@ bool Vector3::is_finite() const {
 }
 
 Vector3::operator String() const {
-	return "(" + String::num_real(x, true) + ", " + String::num_real(y, true) + ", " + String::num_real(z, true) + ")";
+	return String::concat(
+			"(",
+			String::num_real(x, true), ", ",
+			String::num_real(y, true), ", ",
+			String::num_real(z, true),
+			")");
 }
 
 Vector3::operator Vector3i() const {
diff --git a/core/math/vector3i.cpp b/core/math/vector3i.cpp
index 93f9d15ac1bd..0ae80385d10f 100644
--- a/core/math/vector3i.cpp
+++ b/core/math/vector3i.cpp
@@ -70,7 +70,12 @@ Vector3i Vector3i::snappedi(int32_t p_step) const {
 }
 
 Vector3i::operator String() const {
-	return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ")";
+	return String::concat(
+			"(",
+			itos(x), ", ",
+			itos(y), ", ",
+			itos(z),
+			")");
 }
 
 Vector3i::operator Vector3() const {
diff --git a/core/math/vector4.cpp b/core/math/vector4.cpp
index e4fa1769c018..c3b336667b47 100644
--- a/core/math/vector4.cpp
+++ b/core/math/vector4.cpp
@@ -217,7 +217,13 @@ Vector4 Vector4::clampf(real_t p_min, real_t p_max) const {
 }
 
 Vector4::operator String() const {
-	return "(" + String::num_real(x, true) + ", " + String::num_real(y, true) + ", " + String::num_real(z, true) + ", " + String::num_real(w, true) + ")";
+	return String::concat(
+			"(",
+			String::num_real(x, true), ", ",
+			String::num_real(y, true), ", ",
+			String::num_real(z, true), ", ",
+			String::num_real(w, true),
+			")");
 }
 
 static_assert(sizeof(Vector4) == 4 * sizeof(real_t));
diff --git a/core/math/vector4i.cpp b/core/math/vector4i.cpp
index afa77a4988dc..b2153ac9b71f 100644
--- a/core/math/vector4i.cpp
+++ b/core/math/vector4i.cpp
@@ -90,7 +90,13 @@ Vector4i Vector4i::snappedi(int32_t p_step) const {
 }
 
 Vector4i::operator String() const {
-	return "(" + itos(x) + ", " + itos(y) + ", " + itos(z) + ", " + itos(w) + ")";
+	return String::concat(
+			"(",
+			itos(x), ", ",
+			itos(y), ", ",
+			itos(z), ", ",
+			itos(w),
+			")");
 }
 
 Vector4i::operator Vector4() const {
diff --git a/core/string/ustring.h b/core/string/ustring.h
index f72685e704d7..dbb5e364c2fb 100644
--- a/core/string/ustring.h
+++ b/core/string/ustring.h
@@ -607,6 +607,23 @@ class String {
 	// Use `is_valid_ascii_identifier()` instead. Kept for compatibility.
 	bool is_valid_identifier() const { return is_valid_ascii_identifier(); }
 
+	template <typename... Args>
+	void extend(Args... args);
+
+	template <typename... Args>
+	static String concat(Args... args) {
+		String string;
+		string.extend(args...);
+		return string;
+	}
+
+	template <typename... Args>
+	static String concat(String &&string, Args... args) {
+		// Optimized case of the concat call, where we can consume the first argument to avoid re-allocation.
+		string.extend(args...);
+		return std::move(string);
+	}
+
 	/**
 	 * The constructors must not depend on other overloads
 	 */
@@ -669,6 +686,50 @@ String operator+(const char *p_chr, const String &p_str);
 String operator+(const wchar_t *p_chr, const String &p_str);
 String operator+(char32_t p_chr, const String &p_str);
 
+_FORCE_INLINE_ char32_t *_insert_string(const Span<char> &string, char32_t *dst) {
+	const char32_t *src = dst;
+	for (const char32_t *end = dst + string.size(); src < end; ++src, ++dst) {
+		*dst = *src;
+	}
+	return dst;
+}
+
+_FORCE_INLINE_ char32_t *_insert_string(const Span<char32_t> &string, char32_t *dst) {
+	memcpy(dst, string.ptr(), string.size() * sizeof(char32_t));
+	dst += string.size();
+	return dst;
+}
+
+inline Span<char32_t> _to_str_range(const String &string) {
+	return Span<char32_t>(string);
+}
+template <typename Char, typename = std::enable_if_t<std::is_fundamental_v<Char>>>
+Span<Char> _to_str_range(const Span<Char> &string) {
+	return string;
+}
+template <typename Char, size_t len, typename = std::enable_if_t<std::is_fundamental_v<Char>>>
+Span<Char> _to_str_range(const Char (&string)[len]) {
+	return Span(string, strlen(string));
+}
+template <typename Char, typename = std::enable_if_t<std::is_fundamental_v<Char>>>
+Span<Char> _to_str_range(const Char &chr) {
+	return Span<Char>(&chr, 1);
+}
+
+template <typename... Args>
+void _extend_string_ranges(String &string, Args... args) {
+	const int length_before = string.length();
+	string.resize(length_before + (args.len + ...) + 1);
+	char32_t *dst = string.ptrw() + length_before;
+	((dst = _insert_string(args, dst)), ...);
+	*dst = 0;
+}
+
+template <typename... Args>
+void String::extend(Args... args) {
+	_extend_string_ranges(*this, _to_str_range(args)...);
+}
+
 String itos(int64_t p_val);
 String uitos(uint64_t p_val);
 String rtos(double p_val);
diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp
index c05ea7be572c..549a5adf6fa3 100644
--- a/core/variant/variant.cpp
+++ b/core/variant/variant.cpp
@@ -1733,10 +1733,10 @@ String Variant::stringify(int recursion_count) const {
 		}
 		case RID: {
 			const ::RID &s = *reinterpret_cast<const ::RID *>(_data._mem);
-			return "RID(" + itos(s.get_id()) + ")";
+			return String::concat("RID(", itos(s.get_id()), ")");
 		}
 		default: {
-			return "<" + get_type_name(type) + ">";
+			return String::concat("<", get_type_name(type), ">");
 		}
 	}
 }