String manipulation is a very common operation, but the standard C and C++ string libraries have drawbacks. The C++ functions are easy-to-use and powerful, but require too much flash and memory for many embedded projects. The C string functions are lighter weight, but can be difficult to use correctly. Mishandling of null terminators or buffer sizes can result in serious bugs.

The pw_string module provides the flexibility, ease-of-use, and safety of C++-style string manipulation, but with no dynamic memory allocation and a much smaller binary size impact. Using pw_string in place of the standard C functions eliminates issues related to buffer overflow or missing null terminators.





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.






Format instead of snprintf once, return size


Format instead of snprintf 10 times, handle errors


Format instead of snprintf 50 times, no error handling


Safe Length Checking#

This module provides two safer alternatives to std::strlen in case the string is extremely long and/or potentially not null-terminated.

First, a constexpr alternative to C11’s strnlen_s is offerred through pw::string::ClampedCString(). This does not return a length by design and instead returns a string_view which does not require null-termination.

Second, a constexpr specialized form is offered where null termination is required through pw::string::NullTerminatedLength(). This will only return a length if the string is null-terminated.

constexpr std::string_view pw::string::ClampedCString(span<const char> str)#
constexpr std::string_view pw::string::ClampedCString(const char *str, size_t max_len)#

Safe alternative to the string_view constructor to avoid the risk of an unbounded implicit or explicit use of strlen.

This is strongly recommended over using something like C11’s strnlen_s as a string_view does not require null-termination.

constexpr pw::Result<size_t> pw::string::NullTerminatedLength(span<const char> str)#
pw::Result<size_t> pw::string::NullTerminatedLength(const char *str, size_t max_len)#

Safe alternative to strlen to calculate the null-terminated length of the string within the specified span, excluding the null terminator. Like C11’s strnlen_s, the scan for the null-terminator is bounded.


null-terminated length of the string excluding the null terminator. OutOfRange - if the string is not null-terminated.

Precondition: The string shall be at a valid pointer.


The pw::string::Copy functions provide a safer alternative to std::strncpy as it always null-terminates whenever the destination buffer has a non-zero size.

StatusWithSize Copy(const std::string_view &source, span<char> dest)#
StatusWithSize Copy(const char *source, span<char> dest)#
StatusWithSize Copy(const char *source, char *dest, size_t num)#
StatusWithSize Copy(const pw::Vector<char> &source, span<char> dest)#

Copies the source string to the dest, truncating if the full string does not fit. Always null terminates if dest.size() or num > 0.

Returns the number of characters written, excluding the null terminator. If the string is truncated, the status is ResourceExhausted.

Precondition: The destination and source shall not overlap. Precondition: The source shall be a valid pointer.

It also has variants that provide a destination of pw::Vector<char> (see pw_containers for details) that do not store the null terminator in the vector.

StatusWithSize Copy(const std::string_view &source, pw::Vector<char> &dest)#
StatusWithSize Copy(const char *source, pw::Vector<char> &dest)#


The pw::string::PrintableCopy function provides a safe printable copy of a string. It functions with the same safety of pw::string::Copy while also converting any non-printable characters to a . char.

StatusWithSize PrintableCopy(const std::string_view &source, span<char> dest)#


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.

#include "pw_log/log.h"
#include "pw_string/string_builder.h"

pw::Status LogProducedData(std::string_view func_name,
                           span<const std::byte> data) {
  pw::StringBuffer<42> sb;

  // Append a std::string_view to the buffer.
  sb << func_name;

  // Append a format string to the buffer.
  sb.Format(" produced %d bytes of data: ", static_cast<int>(;

  // Append bytes as hex to the buffer.
  sb << data;

  // Log the final string.
  PW_LOG_DEBUG("%s", sb.c_str());

  // Errors encountered while mutating the string builder are tracked.
  return sb.status();

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:

namespace my_project {

struct MyType {
  int foo;
  const char* bar;

pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
  return sb << "MyType(" << << ", " << << ')';

}  // 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:

#include "pw_string/to_string.h"

namespace pw {

template <>
StatusWithSize ToString<MyStatus>(MyStatus value, span<char> buffer) {
  return Copy(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.






Total StringBuilder cost when used alongside snprintf


StringBuilder cost when completely replacing snprintf


Incremental cost relative to snprintf for 10 strings


Module Configuration Options#

The following configuration options can be adjusted via compile-time configuration of this module.


Setting this to a non-zero value will result in the ToString function outputting string representations of floating-point values with a decimal expansion after the point, by using the Format function. The default value of this configuration option is zero, which will result in floating point values being rounded to the nearest integer in their string representation.

Using a non-zero value for this configuration option may incur a code size cost due to the dependency on Format.

Future work#

  • StringBuilder’s fixed size cost can be dramatically reduced by limiting support for 64-bit integers.

  • Consider integrating with the tokenizer module.


To enable pw_string for Zephyr add CONFIG_PIGWEED_STRING=y to the project’s configuration.