Substring
Pass by reference
Programming
C++
Code efficiency

How can I pass a substring by reference?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Passing a substring “by reference” is really a question about avoiding copies while still referring to part of an existing string. In C++, the right answer is usually not std::string&, because a substring is not a separate std::string object inside the original string. The practical tool is a non-owning view such as std::string_view, or a pair of iterators or indices when mutability matters.

Why std::string& Does Not Solve This

A reference such as std::string& binds to an actual std::string object. But text.substr(2, 5) produces a new string object by value, which means it copies characters.

cpp
1#include <iostream>
2#include <string>
3
4void print_text(const std::string& s) {
5    std::cout << s << '\n';
6}
7
8int main() {
9    std::string text = "Hello, world";
10    print_text(text.substr(7, 5));
11}

This compiles and works, but it does not avoid allocation. The substring is a temporary string.

So if your real goal is “refer to a slice without copying,” you need a view, not a reference to a new string object.

Use std::string_view for Read-Only Substrings

For read-only access, std::string_view is usually the best tool.

cpp
1#include <iostream>
2#include <string>
3#include <string_view>
4
5void print_view(std::string_view view) {
6    std::cout << view << '\n';
7}
8
9int main() {
10    std::string text = "Hello, world";
11    std::string_view view(text.data() + 7, 5);
12    print_view(view);
13}

This prints world without allocating a new string.

A cleaner pattern is to create a view over the whole string first and then slice the view:

cpp
1#include <iostream>
2#include <string>
3#include <string_view>
4
5int main() {
6    std::string text = "abcdefg";
7    std::string_view all = text;
8    std::string_view part = all.substr(2, 3);
9
10    std::cout << part << '\n';
11}

That is efficient and expressive.

Lifetime Rules Matter

std::string_view does not own the characters it points to. That means the original string must stay alive and unchanged in ways that would invalidate the view.

This is safe:

cpp
std::string text = "example";
std::string_view part = std::string_view(text).substr(1, 3);

This is dangerous:

cpp
std::string_view bad = std::string("example").substr(1, 3);

The temporary string is destroyed immediately, leaving bad dangling. This is the main tradeoff of view-based substring passing.

If You Need to Modify the Original String

A substring view is not the right abstraction if the callee needs to mutate the original characters through that slice. In that case, pass the original string plus range information.

cpp
1#include <iostream>
2#include <string>
3
4void make_upper(std::string& text, std::size_t start, std::size_t len) {
5    for (std::size_t i = start; i < start + len && i < text.size(); ++i) {
6        text[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[i])));
7    }
8}
9
10int main() {
11    std::string text = "hello world";
12    make_upper(text, 6, 5);
13    std::cout << text << '\n';
14}

This changes the original string in place. There is no separate “substring reference” object in the standard library that behaves like a writable std::string& slice.

Iterators Are Another Option

For algorithms, iterator pairs can be a good fit:

cpp
1#include <algorithm>
2#include <cctype>
3#include <iostream>
4#include <string>
5
6void make_upper(std::string::iterator first, std::string::iterator last) {
7    std::transform(first, last, first, [](unsigned char c) {
8        return static_cast<char>(std::toupper(c));
9    });
10}
11
12int main() {
13    std::string text = "hello world";
14    make_upper(text.begin() + 6, text.begin() + 11);
15    std::cout << text << '\n';
16}

This is often a good design when you want algorithm-style APIs that operate on ranges rather than string types specifically.

Choose the Tool Based on Intent

Use these rules:

  • for read-only, zero-copy substring access, use std::string_view
  • for writable access, pass the original string plus indices or iterators
  • if ownership is required, accept a real std::string copy

That is much clearer than trying to force every substring problem into “pass by reference” terminology.

Common Pitfalls

The biggest pitfall is assuming substr() returns a reference into the original string. It returns a new string object.

Another mistake is creating a std::string_view from a temporary string and then using it after the temporary has been destroyed.

Developers also sometimes use std::string_view for writable APIs. It is read-only by design.

Finally, if you need stable references across string mutation, remember that resizing or reallocation can invalidate views and iterators.

Summary

  • A substring is not a built-in referenceable subobject of std::string.
  • 'std::string_view is the usual zero-copy solution for read-only substring access.'
  • For mutation, pass the original string with indices or iterators.
  • 'substr() creates a new string and does not avoid copying.'
  • Always respect lifetime and invalidation rules when using views or iterators.

Course illustration
Course illustration

All Rights Reserved.