Substring
Pass by reference
Programming
C++
Code efficiency

How can I pass a substring by reference?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions