pw_string: Guide#

InlineString and StringBuilder?#

Pigweed AI summary: This text discusses two string classes in the PW library: InlineString and StringBuilder. InlineString is recommended for compatibility with std::string, internal storage, persisting in data structures, and lower code size overhead. StringBuilder is recommended for compatibility with std::ostringstream, external storage, non-fatal handling of failed operations, tracking operation status, and temporary stack objects. Examples are given for when to use each class.

Use pw::InlineString if you need:

  • Compatibility with std::string

  • Storage internal to the object

  • A string object to persist in other data structures

  • Lower code size overhead

Use pw::StringBuilder if you need:

  • Compatibility with std::ostringstream, including custom object support

  • Storage external to the object

  • Non-fatal handling of failed append/format operations

  • Tracking of the status of a series of operations

  • A temporary stack object to aid string construction

  • Medium code size overhead

An example of when to prefer pw::InlineString is wrapping a length-delimited string (e.g. std::string_view) for APIs that require null termination:

#include <string>
#include "pw_log/log.h"
#include "pw_string/string_builder.h"

void ProcessName(std::string_view name) {
  // %s format strings require null terminated strings, so create one on the
  // stack with size up to kMaxNameLen, copy the string view `name` contents
  // into it, add a null terminator, and log it.
  PW_LOG_DEBUG("The name is %s",
               pw::InlineString<kMaxNameLen>(name).c_str());
}

An example of when to prefer pw::StringBuilder is when constructing a string for external use.

#include "pw_string/string_builder.h"

pw::Status FlushSensorValueToUart(int32_t sensor_value) {
  pw::StringBuffer<42> sb;
  sb << "Sensor value: ";
  sb << sensor_value;  // Formats as int.
  FlushCStringToUart(sb.c_str());

  if (!sb.status().ok) {
    format_error_metric.Increment();  // Track overflows.
  }
  return sb.status();
}

Building strings with pw::StringBuilder#

Pigweed AI summary: This section provides an example of how to use pw::StringBuilder to build strings in C++. It includes code that demonstrates how to allocate a buffer, append strings and format strings, and log the final string. The example also shows how errors encountered while building the string are tracked.

The following shows basic use of a pw::StringBuilder.

#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 allocates a pw::StringBuilder with a built-in buffer.
  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>(data.data()));

  // 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();
}

Building strings with pw::InlineString#

Pigweed AI summary: This article discusses the use of pw::InlineString objects in C++ programming. These objects require a fixed capacity to be specified during construction and can be used to create null-terminated strings for APIs that expect them. The article provides examples of initializing and manipulating InlineString objects, including copying into known-capacity strings and using helpful std::string functions. InlineString also integrates well with std::string_view and supports implicit conversions to and from it.

pw::InlineString objects must be constructed by specifying a fixed capacity for the string.

#include "pw_string/string.h"

// Initialize from a C string.
pw::InlineString<32> inline_string = "Literally";
inline_string.append('?', 3);   // contains "Literally???"

// Supports copying into known-capacity strings.
pw::InlineString<64> other = inline_string;

// Supports various helpful std::string functions
if (inline_string.starts_with("Lit") || inline_string == "not\0literally"sv) {
  other += inline_string;
}

// Like std::string, InlineString is always null terminated when accessed
// through c_str(). InlineString can be used to null-terminate
// length-delimited strings for APIs that expect null-terminated strings.
std::string_view file(".gif");
if (std::fopen(pw::InlineString<kMaxNameLen>(file).c_str(), "r") == nullptr) {
  return;
}

// pw::InlineString integrates well with std::string_view. It supports
// implicit conversions to and from std::string_view.
inline_string = std::string_view("not\0literally", 12);

FunctionThatTakesAStringView(inline_string);

FunctionThatTakesAnInlineString(std::string_view("1234", 4));

Building strings inside InlineString with a StringBuilder#

Pigweed AI summary: The article discusses how to build strings inside InlineString using pw::StringBuilder in C++. It provides an example code snippet that demonstrates how to use pw::StringBuilder to build a string in an InlineString.

pw::StringBuilder can build a string in a pw::InlineString:

#include "pw_string/string.h"

void DoFoo() {
  InlineString<32> inline_str;
  StringBuilder sb(inline_str);
  sb << 123 << "456";
  // inline_str contains "456"
}

Passing InlineStrings as parameters#

Pigweed AI summary: This section discusses how to pass InlineString objects as parameters to non-templated functions via type erasure, which can save code size. It also covers how to operate on InlineString objects without knowing their type using the pw::InlineString<> type, and how generically sized InlineString objects don't work in constexpr contexts. Finally, it explains how operations on known-size strings may be used in constexpr expressions.

pw::InlineString objects can be passed to non-templated functions via type erasure. This saves code size in most cases, since it avoids template expansions triggered by string size differences.

Unknown size strings#

Pigweed AI summary: This section explains how to operate on pw::InlineString objects without knowing their type by using the pw::InlineString<> type. It provides examples of how to remove suffixes from generically-sized InlineStrings and explains that these objects don't work in constexpr contexts.

To operate on pw::InlineString objects without knowing their type, use the pw::InlineString<> type, shown in the examples below:

// Note that the first argument is a generically-sized InlineString.
void RemoveSuffix(pw::InlineString<>& string, std::string_view suffix) {
  if (string.ends_with(suffix)) {
     string.resize(string.size() - suffix.size());
  }
}

void DoStuff() {
  pw::InlineString<32> str1 = "Good morning!";
  RemoveSuffix(str1, " morning!");

  pw::InlineString<40> str2 = "Good";
  RemoveSuffix(str2, " morning!");

  PW_ASSERT(str1 == str2);
}

However, generically sized pw::InlineString objects don’t work in constexpr contexts.

Known size strings#

Pigweed AI summary: The article discusses the use of known-size strings in constexpr expressions. It mentions the pw::InlineString operations that can be used for this purpose. It also provides an example of a constexpr expression using a static constexpr pw::InlineString.

pw::InlineString operations on known-size strings may be used in constexpr expressions.

static constexpr pw::InlineString<64> kMyString = [] {
  pw::InlineString<64> string;

  for (int i = 0; i < 10; ++i) {
    string += "Hello";
  }

  return string;
}();

Compact initialization of InlineStrings#

Pigweed AI summary: The article discusses the support of class template argument deduction (CTAD) in C++17 and newer for pw::InlineBasicString. However, CTAD is not supported until C++20 for pw::InlineString, which is an alias. The article provides an example of how CTAD can be used with pw::InlineBasicString and how it can be used with pw::InlineString in C++20.

pw::InlineBasicString supports class template argument deduction (CTAD) in C++17 and newer. Since pw::InlineString is an alias, CTAD is not supported until C++20.

// Deduces a capacity of 5 characters to match the 5-character string literal
// (not counting the null terminator).
pw::InlineBasicString inline_string = "12345";

// In C++20, CTAD may be used with the pw::InlineString alias.
pw::InlineString my_other_string("123456789");

Supporting custom types with StringBuilder#

Pigweed AI summary: The article explains how to use StringBuilder to print custom types by defining the operator<< in the same namespace as the custom type. It also mentions that the ToString function can be specialized to support custom types with StringBuilder, but it is recommended to overload operator<< instead. The article provides examples of how to do this.

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(" << 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:

#include "pw_string/to_string.h"

namespace pw {

template <>
StatusWithSize ToString<MyStatus>(MyStatus value, span<char> buffer) {
  return Copy(MyStatusString(value), buffer);
}

}  // namespace pw

Saving code space by replacing snprintf#

The C standard library function snprintf is commonly used for string formatting. However, it isn’t optimized for embedded systems, and using it will bring in a lot of other standard library code that will inflate your binary size.

Size comparison: snprintf versus pw::StringBuilder#

Pigweed AI summary: The code size cost of using pw::StringBuilder is smaller than using std::snprintf. By using pw::StringBuilder's << and append methods instead of snprintf, significant code size reductions can be achieved. However, there are cases where the incremental code size cost of pw::StringBuilder is similar to that of snprintf. In these cases, using one or two pw::StringBuilder appends may still have a smaller code size impact than a single snprintf call. Additionally, using pw::StringBuilder error handling will also impact

The fixed code size cost of pw::StringBuilder is smaller than that of std::snprintf. Using only pw::StringBuilder’s << and append methods instead of snprintf leads to significant code size reductions.

However, there are cases when the incremental code size cost of pw::StringBuilder is similar to that of snprintf. For example, each argument to pw::StringBuilder’s << method expands to a function call, but one or two pw::StringBuilder appends may still have a smaller code size impact than a single snprintf call. Using pw::StringBuilder error handling will also impact code size in a way that is comparable to snprintf.

Label

Segment

Delta

Total StringBuilder cost when used alongside snprintf

FLASH

+48

[section .code]

-2

_ctype_

+4

quorem

+2

__mprec_tens

+92

main

+4

CSWTCH.1

-2

pw_varint_ZigZagEncode

-4

p05.0

NEW

+698

__udivmoddi4

NEW

+276

pw::string::IntToString<>()

NEW

+160

pw::string::(anonymous namespace)::kPowersOf10

NEW

+88

pw::StringBuilder::append()

NEW

+64

pw::string::DecimalDigitCount()

NEW

+60

pw::StringBuilder::ResizeAndTerminate()

NEW

+26

pw::StringBuilder::NullTerminate()

NEW

+24

pw::string::(anonymous namespace)::HandleExhaustedBuffer()

NEW

+20

pw::StringBuilder::HandleStatusWithSize()

NEW

+2

__aeabi_ldiv0

+1,560

StringBuilder cost when completely replacing snprintf

FLASH

+55

[section .code]

DEL

-504

_svfprintf_r

-3

_ctype_

-8

__mprec_tens

DEL

-184

__ssputs_r

DEL

-104

snprintf

DEL

-102

pw::allocator::FreeListHeap::Realloc()

+92

main

+2

pw::allocator::FreeList::RemoveChunk()

-2

pw_varint_ZigZagEncode

DEL

-16

__wrap__realloc_r

+4

p05.0

NEW

+698

__udivmoddi4

NEW

+276

pw::string::IntToString<>()

NEW

+160

pw::string::(anonymous namespace)::kPowersOf10

NEW

+88

pw::StringBuilder::append()

NEW

+64

pw::string::DecimalDigitCount()

NEW

+60

pw::StringBuilder::ResizeAndTerminate()

NEW

+26

pw::StringBuilder::NullTerminate()

NEW

+24

pw::string::(anonymous namespace)::HandleExhaustedBuffer()

NEW

+20

pw::StringBuilder::HandleStatusWithSize()

NEW

+2

__aeabi_ldiv0

+648

Incremental cost relative to snprintf for 10 strings

FLASH

-44

[section .code]

-100

main

-4

CSWTCH.1

+4

pw::string::DecimalDigitCount()

NEW

+112

pw::StringBuilder::operator<< <>()

NEW

+8

pw::StringBuilder::push_back()

-24

Size comparison: snprintf versus pw::string::Format#

Pigweed AI summary: The article discusses the size comparison between the pw::string::Format functions and std::snprintf calls. It states that the pw::string::Format functions have a small, fixed code size cost and there is no incremental code size cost to using them relative to equivalent std::snprintf calls. The article also provides a table that compares the code size of using pw::string::Format versus std::snprintf in different scenarios.

The pw::string::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 pw::string::Format.

Label

Segment

Delta

Format instead of snprintf once, return size

FLASH

-4

quorem

DEL

-104

snprintf

-12

pw::string::OutputStringsToBuffer()

-2

pw_varint_ZigZagEncode

NEW

+88

_vsnprintf_r

NEW

+56

pw::string::FormatVaList()

NEW

+38

pw::string::Format()

NEW

+28

vsnprintf

+88

Format instead of snprintf 10 times, handle errors

FLASH

+4

[section .code]

-32

pw::string::OutputStringsToBuffer()

DEL

-104

snprintf

DEL

-44

(anonymous namespace)::ProcessResult()

-2

pw_varint_ZigZagEncode

NEW

+88

_vsnprintf_r

NEW

+56

pw::string::FormatVaList()

NEW

+38

pw::string::Format()

NEW

+28

vsnprintf

+32

Format instead of snprintf 50 times, no error handling

FLASH

-8

pw::string::OutputStringsToBuffer()

DEL

-104

snprintf

-2

pw_varint_ZigZagEncode

NEW

+88

_vsnprintf_r

NEW

+56

pw::string::FormatVaList()

NEW

+38

pw::string::Format()

NEW

+28

vsnprintf

+96