pw_string: Update docs & test operator<< overload
- Expand the documentation for overloading StringBuilder's operator<<
for custom types. Move some documentation from code comments to .rst.
- Add tests for overloading operator<< for StringBuilder.
Change-Id: Ida9c9d8bdcc6aee645e38ef896b1eaf34b133d00
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/34800
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Paul Mathieu <paulmathieu@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index 8944100..467908b 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -19,24 +19,15 @@
=============
C++17
-Dependencies
-============
-* ``pw_preprocessor``
-* ``pw_status``
-* ``pw_span``
-
-Features
-========
-
pw::string::Format
-------------------
+==================
The ``pw::string::Format`` and ``pw::string::FormatVaList`` functions provide
safer alternatives to ``std::snprintf`` and ``std::vsnprintf``. The snprintf
return value is awkward to interpret, and misinterpreting it can lead to serious
bugs.
Size report: replacing snprintf with pw::string::Format
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-------------------------------------------------------
The ``Format`` functions have a small, fixed code size cost. However, relative
to equivalent ``std::snprintf`` calls, there is no incremental code size cost to
using ``Format``.
@@ -44,28 +35,70 @@
.. include:: format_size_report
pw::StringBuilder
------------------
-StringBuilder facilitates building formatted strings in a fixed-size buffer. It
-is designed to give the flexibility of ``std::string`` and
-``std::ostringstream``, but with a small footprint. However, applications
-sensitive to code size should use StringBuilder with care.
+=================
+``pw::StringBuilder`` facilitates building formatted strings in a fixed-size
+buffer. It is designed to give the flexibility of ``std::string`` and
+``std::ostringstream``, but with a small footprint.
+
+Supporting custom types with StringBuilder
+------------------------------------------
+As with ``std::ostream``, StringBuilder supports printing custom types by
+overriding the ``<<`` operator. This is is done by defining ``operator<<`` in
+the same namespace as the custom type. For example:
+
+.. code-block:: cpp
+
+ namespace my_project {
+
+ struct MyType {
+ int foo;
+ const char* bar;
+ };
+
+ pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
+ return sb << "MyType(" << value.foo << ", " << value.bar << ')';
+ }
+
+ } // namespace my_project
+
+Internally, ``StringBuilder`` uses the ``ToString`` function to print. The
+``ToString`` template function can be specialized to support custom types with
+``StringBuilder``, though it is recommended to overload ``operator<<`` instead.
+This example shows how to specialize ``pw::ToString``:
+
+.. code-block:: cpp
+
+ #include "pw_string/to_string.h"
+
+ namespace pw {
+
+ template <>
+ StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
+ return CopyString(MyStatusString(value), buffer);
+ }
+
+ } // namespace pw
Size report: replacing snprintf with pw::StringBuilder
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+------------------------------------------------------
+StringBuilder is safe, flexible, and results in much smaller code size than
+using ``std::ostringstream``. However, applications sensitive to code size
+should use StringBuilder with care.
+
The fixed code size cost of StringBuilder is significant, though smaller than
``std::snprintf``. Using StringBuilder's << and append methods exclusively in
place of ``snprintf`` reduces code size, but ``snprintf`` may be difficult to
avoid.
The incremental code size cost of StringBuilder is comparable to ``snprintf`` if
-errors are handled. Each argument to StringBuilder's << expands to a function
-call, but one or two StringBuilder appends may have a smaller code size impact
-than a single ``snprintf`` call.
+errors are handled. Each argument to StringBuilder's ``<<`` expands to a
+function call, but one or two StringBuilder appends may have a smaller code size
+impact than a single ``snprintf`` call.
.. include:: string_builder_size_report
Future work
-^^^^^^^^^^^
+===========
* StringBuilder's fixed size cost can be dramatically reduced by limiting
support for 64-bit integers.
* Consider integrating with the tokenizer module.
diff --git a/pw_string/public/pw_string/string_builder.h b/pw_string/public/pw_string/string_builder.h
index 7d2c5a4..47db98f 100644
--- a/pw_string/public/pw_string/string_builder.h
+++ b/pw_string/public/pw_string/string_builder.h
@@ -41,12 +41,25 @@
// StringBuilder supports C++-style << output, similar to std::ostringstream. It
// also supports std::string-like append functions and printf-style output.
//
-// StringBuilder uses the ToString function to support arbitrary types. Defining
-// a ToString template specialization overload in the pw namespace allows
-// writing that type to a StringBuilder with <<.
+// Support for custom types is added by overloading operator<< in the same
+// namespace as the custom type. For example:
//
-// For example, the following ToString overload allows writing MyStatus objects
-// to StringBuilders:
+// namespace my_project {
+//
+// struct MyType {
+// int foo;
+// const char* bar;
+// };
+//
+// pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
+// return sb << "MyType(" << value.foo << ", " << value.bar << ')';
+// }
+//
+// } // namespace my_project
+//
+// The ToString template function can be specialized to support custom types
+// with StringBuilder, though overloading operator<< is generally preferred. For
+// example:
//
// namespace pw {
//
@@ -57,32 +70,6 @@
//
// } // namespace pw
//
-// For complex types, it may be easier to override StringBuilder's << operator,
-// similar to the standard library's std::ostream. For example:
-//
-// namespace pw {
-//
-// StringBuilder& operator<<(StringBuilder& sb, const MyType& value) {
-// return sb << "MyType(" << value.foo << ", " << value.bar << ')';
-// }
-//
-// } // namespace pw
-//
-// Alternately, complex types may use a StringBuilder in their ToString, but it
-// is likely to be simpler to override StringBuilder's operator<<.
-//
-// StringBuilder is safe, flexible, and results in much smaller code size than
-// using std::ostringstream. However, applications sensitive to code size should
-// use StringBuilder with care.
-//
-// The fixed code size cost of StringBuilder is significant, though smaller than
-// std::snprintf. Using StringBuilder's << and append methods exclusively in
-// place of snprintf reduces code size, but snprintf may be difficult to avoid.
-//
-// The incremental code size cost of StringBuilder is comparable to snprintf if
-// errors are handled. Each argument to StringBuilder's << expands to a function
-// call, but one or two StringBuilder appends may have a smaller code size
-// impact than a single snprintf call. See the size report for further analysis.
class StringBuilder {
public:
// Creates an empty StringBuilder.
@@ -282,7 +269,7 @@
// str.c_str(); // null terminated C string "The answer is 42."
// str.view(); // std::string_view of "The answer is 42."
//
-template <size_t kSizeBytes>
+template <size_t size_bytes>
class StringBuffer : public StringBuilder {
public:
StringBuffer() : StringBuilder(buffer_) {}
@@ -308,7 +295,7 @@
}
StringBuffer& operator=(const StringBuffer& other) {
- assign<kSizeBytes>(other);
+ assign<size_bytes>(other);
return *this;
}
@@ -322,9 +309,9 @@
}
// Returns the maximum length of the string, excluding the null terminator.
- static constexpr size_t max_size() { return kSizeBytes - 1; }
+ static constexpr size_t max_size() { return size_bytes - 1; }
- // Returns a StringBuffer<kSizeBytes>& instead of a generic StringBuilder& for
+ // Returns a StringBuffer<size_bytes>& instead of a generic StringBuilder& for
// append calls and stream-style operations.
template <typename... Args>
StringBuffer& append(Args&&... args) {
@@ -344,8 +331,8 @@
std::memcpy(buffer_, other.data(), other.size() + 1); // include the \0
}
- static_assert(kSizeBytes >= 1u, "StringBuffers must be at least 1 byte long");
- char buffer_[kSizeBytes];
+ static_assert(size_bytes >= 1u, "StringBuffers must be at least 1 byte long");
+ char buffer_[size_bytes];
};
namespace string_internal {
diff --git a/pw_string/string_builder_test.cc b/pw_string/string_builder_test.cc
index 22378cc..ee8bad5 100644
--- a/pw_string/string_builder_test.cc
+++ b/pw_string/string_builder_test.cc
@@ -582,5 +582,42 @@
25);
static_assert(DefaultStringBufferSize('a', nullptr, 'b', 4, 5, 6, 7, 8) == 33);
+struct SomeCustomType {};
+
+StringBuilder& operator<<(StringBuilder& sb, const SomeCustomType&) {
+ return sb << "SomeCustomType was here!";
+}
+
+TEST(StringBuilder, ShiftOperatorOverload_SameNamsepace) {
+ pw::StringBuffer<48> buffer;
+ buffer << SomeCustomType{};
+
+ EXPECT_STREQ("SomeCustomType was here!", buffer.c_str());
+}
+
} // namespace
} // namespace pw
+
+namespace some_other_ns {
+
+struct MyCustomType {
+ int item;
+};
+
+pw::StringBuilder& operator<<(pw::StringBuilder& sb,
+ const MyCustomType& value) {
+ return sb << "MyCustomType(" << value.item << ')';
+}
+
+} // namespace some_other_ns
+
+namespace pw_test_namespace {
+
+TEST(StringBuilder, ShiftOperatorOverload_DifferentNamsepace) {
+ pw::StringBuffer<48> buffer;
+ buffer << "This is " << some_other_ns::MyCustomType{1138};
+
+ EXPECT_STREQ("This is MyCustomType(1138)", buffer.data());
+}
+
+} // namespace pw_test_namespace