August 6, 20257 min

Where do string literals (&str) live?

m
mayo

String literals (&str) in Rust are handled efficiently, with distinct memory characteristics compared to heap-allocated String types. Understanding their allocation and lifetime is key to writing performant and safe Rust code.

s: &'static str ptr + len (stack) name: String ptr + len + cap (stack) .rodata segment "hello" — immutable, 'static Heap "Alice and Bob" — mutable, scoped Returning &str borrowed from a local String dangles when the function returns

String Literals (&str) in Memory

Storage Location

  • String literals (e.g., "hello") are stored in the read-only data segment (.rodata) of the compiled binary, not on the heap or stack.
  • They are embedded directly in the executable and loaded into memory at program startup.
  • Memory is static, meaning it lives for the entire program duration.

Type Inference

  • The type of "hello" is &'static str:
    • &str: An immutable string slice.
    • 'static: A lifetime lasting the entire program.

Example: Memory Layout:

let s: &'static str = "hello"; // Points to static memory
  • Binary Representation:
    • Executable Memory: "hello" stored in .rodata section, e.g., at address 0x1000.
    • Variable s: A pointer (0x1000) + length (5), stored on the stack.

Key Properties

Property Explanation
Immutable Cannot modify the literal (e.g., "hello"[0] = 'H' is forbidden).
Zero-Cost No runtime allocation (already in memory).
Lifetime Always 'static (valid for the whole program).

Comparison with String

Feature &'static str (literal) String
Memory Location Read-only data segment Heap
Mutability Immutable Mutable
Lifetime 'static Scoped to owner
Allocation Cost None (compile-time) Runtime allocation

Common Use Cases

Constants

const GREETING: &str = "hello"; // No allocation

Function Arguments

Prefer &str over &String to accept literals without allocation:

fn print(s: &str) { /* ... */ }
print("world"); // No conversion needed

Why Not Always Use &'static str?

  • Limited to compile-time-known strings.
  • Cannot dynamically create or modify them (unlike String).

Example: Dynamic Strings Require String:

let name = "Alice".to_string(); // Heap-allocated copy
name.push_str(" and Bob");      // Mutability possible

The Problem: Dangling Pointer Risk

Returning a reference (&str) to a local String creates a dangling pointer, as the String is dropped when the function ends.

Example: Code That Fails to Compile:

fn return_str() -> &str {         // ERROR: Missing lifetime specifier!
    let s = String::from("hello");
    &s                            // Returns a reference to `s`...
}                                 // `s` is dropped here (dangling pointer!)

Compiler Error:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:17
  |
1 | fn return_str() -> &str {
  |                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Why Rust Rejects This

  • Ownership Rules: String (s) is owned by the function and dropped when the scope ends. Returning &s would create a reference to freed memory.
  • Lifetime Enforcement: Rust requires explicit lifetimes to ensure references are always valid. Here, the reference (&str) has no owner to borrow from after the function exits.

How to Fix It

There are exactly three shapes the fix can take, and they differ in who owns the bytes:

fn return_str() -> &str nothing for the &str to borrow from -> String ownership moves to caller costs one heap allocation -> &'static str bytes already sit in .rodata no allocation, literals only -> Cow<'static, str> borrowed or owned per call allocates only when it must The local String is never an option: it dies at the closing brace. Every fix either hands over ownership or points at memory that outlives the call.

Option 1: Return an Owned String (No Reference)

fn return_owned() -> String {  // Transfer ownership to caller
    String::from("hello")      // No reference, no lifetime issue
}

Option 2: Return a &'static str (String Literal)

fn return_static() -> &'static str {  // Lives forever in binary
    "hello"                          // Static memory (not heap)
}

Option 3: Use Cow<str> for Flexibility

use std::borrow::Cow;

fn return_cow(is_heap: bool) -> Cow<'static, str> {
    if is_heap {
        Cow::Owned(String::from("hello"))  // Heap-allocated
    } else {
        Cow::Borrowed("hello")             // Static memory
    }
}

Key Takeaways

String literals:

  • Live in static memory (part of the binary).
  • Are immutable and zero-cost.
  • Have 'static lifetime.

🚀 When to use them:

  • For fixed, read-only strings (e.g., messages, constants).
  • To avoid allocations in function APIs (&str over &String).

Never return &str borrowed from a local String—it’s impossible in safe Rust.

Solutions:

  • Return String (ownership transfer).
  • Use &'static str (literals only).
  • Use Cow<str> for dynamic choices.

Advanced Note: Rust optimizes &str references to literals. Even if you write:

let s = String::from("hello");
let slice = &s[..]; // Points to heap, not static memory!

The compiler may elide copies if the content is known statically.

Experiment: What happens if you try returning &s[..] instead of &s?
Answer: No—it’s the same issue! The slice still points to the doomed String.