me-wiki
Log in

Strings

Rust String Types Cheat Sheet

Type Memory Location Size on Stack Ownership Reallocation / Mutation Cost
&'static str Read-Only Data Segment (.rodata) 16 bytes (ptr + len) Borrowed indefinitely; zero allocation Immutable; cannot grow in place
String Heap (buffer) + Stack (fat pointer) 24 bytes (ptr + cap + len) Owned buffer; drops memory on scope exit Can grow in place; reallocates when len > cap
Cow<'static, str> Stack enum (Borrowed vs Owned) 24 bytes (enum discriminant + data payload) Lazily owned; borrows until mutation or ownership is required Zero-cost until .to_mut() triggers an allocation

Mechanical Breakdown & Usage

1. &'static str

An immutable byte slice pointing to memory embedded directly in the compiled binary. It incurs zero runtime allocation cost and cannot be dropped.

fn print_static_label() {
    let label: &'static str = "ENGINE_INIT_OK";
    println!("{label}");
}

  • Use case: Hardcoded UI labels, static error codes, system constants.
  • Avoid when: Values are dynamically parsed or constructed at runtime.

2. String

A growable, UTF-8 encoded heap buffer managed by a 3-word struct on the stack.

fn format_system_metric(core_id: usize, usage: f32) -> String {
    format!("Core {core_id}: {usage:.2}%")
}

  • Use case: Dynamic user inputs, JSON payload parsing, runtime string mutation.
  • Avoid when: Passing read-only slices into functions where borrowing suffices.

3. Cow<'static, str>

A clone-on-write smart pointer defined as enum Cow<'a, B> { Borrowed(&'a B), Owned(<B ToOwned as>::Owned) }. It acts as an optimization layer avoiding heap allocations when processing static defaults, switching to an owned String only when mutation or dynamic data is present.

use std::borrow::Cow;

fn sanitize_route(path: &'static str) -> Cow<'static, str> {
    if path.starts_with('/') {
        Cow::Borrowed(path) // Zero allocations
    } else {
        Cow::Owned(format!("/{path}")) // Allocates only when modification is strictly necessary
    }
}

  • Use case: Config registries with static fallbacks, UI element labels that are usually static strings but occasionally formatted dynamically.
  • Avoid when: Data is guaranteed to always be dynamically allocated (use String) or guaranteed to always be static (use &'static str).

Generic Functions for Zero-Allocation & Flexible APIs

Accepting String in function arguments forces callers with string literals to call .to_string(), triggering unnecessary malloc/memcpy cycles.

Use standard library abstraction traits depending on whether the function requires read-only inspection, ownership, or conditional mutation.

Pattern A: Read-Only Access via AsRef<str>

Zero heap allocation. Accepts &str, &'static str, String, Cow<str>, and &String without taking ownership.

fn log_component_event<S: AsRef<str>>(component: &str, event: S) {
    let event_slice: &str = event.as_ref();
    println!("[{component}] {event_slice}");
}

// Caller ergonomics:
log_component_event("Renderer", "Frame Dropped");         // &'static str (0 allocs)
log_component_event("Renderer", format!("Code: {0}", 42)); // String (1 alloc from format)

Pattern B: Conversion to Owned Storage via Into<String>

When a struct or collection must own the data, take Into<String>. Callers passing an existing String yield ownership directly without cloning; callers passing a string literal pay for exactly one allocation at the call site.

#[derive(Debug)]
pub struct WidgetConfig {
    pub label: String,
}

impl WidgetConfig {
    pub fn new<S: Into<String>>(label: S) -> Self {
        Self {
            label: label.into(),
        }
    }
}

// Caller ergonomics:
let c1 = WidgetConfig::new("Static Label"); // Allocates String only on entry
let owned = String::from("Dynamic Label");
let c2 = WidgetConfig::new(owned);          // Zero reallocation; takes ownership of existing buffer

Pattern C: Maximizing Cache/Memory Efficiency via Into<Cow<'a, str>>

Optimal for structs that can hold either a static slice or an owned string, completely bypassing the heap when initialized with string literals while remaining compatible with dynamic data.

use std::borrow::Cow;

#[derive(Debug, PartialEq, Eq)]
pub struct UiTextNode<'a> {
    pub content: Cow<'a, str>,
}

impl<'a> UiTextNode<'a> {
    pub fn new<S: Into<Cow<'a, str>>>(content: S) -> Self {
        Self {
            content: content.into(),
        }
    }
}

// Caller ergonomics:
// 1. Literal path: Zero heap allocations, points directly to binary's data segment
let static_node = UiTextNode::new("Title"); 
assert!(matches!(static_node.content, Cow::Borrowed(_)));

// 2. Dynamic path: Takes ownership of the heap buffer seamlessly
let dynamic_node = UiTextNode::new(format!("Count: {}", 100));
assert!(matches!(dynamic_node.content, Cow::Owned(_)));