Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 2 – Types, values and expressions

std::string and std::string_view

Build, change and search text with std::string, pass it cheaply as std::string_view, and catch the views that outlive their text before they print garbage.

  • Beginner
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Create, modify and search strings
  • Pass read-only text efficiently with std::string_view
  • Avoid string_views that outlive the text they point to

Before you start

std::string owns a sequence of characters: it allocates the memory, grows it as you append, and frees it when the string is destroyed, so it is the type for text you build, change or keep. std::string_view (C++17) owns nothing: it is a pointer and a length that refer to characters someone else owns, so making one, or taking a piece of one, copies nothing. That makes std::string_view the usual type for a function parameter that only reads text: it accepts a std::string, a string literal or a part of either without a copy. The price is lifetime. A view that outlives its text points at freed memory, and the compiler usually says nothing. This lesson shows the operations of both types, measures what the view saves, records how short strings are stored by the two standard libraries, and reproduces the dangling view under AddressSanitizer, with Apple clang 21 and GCC 16.

Building and changing a std::string

A std::string behaves like a value: copying it copies the characters, and comparing two strings compares their characters. The common operations, one per line:

editing.cpp C++
// Building and changing a std::string: the string owns its characters and grows as needed.
#include <print>
#include <stdexcept>
#include <string>

int main() {
    std::string order = "masala dosa";
    order += ", filter coffee";              // append
    order.insert(0, "2 x ");                 // insert at a position
    std::println("{} ({} characters)", order, order.size());

    order.replace(0, 1, "3");                // replace 1 character at position 0
    order.erase(order.find(','));            // erase from the comma to the end
    std::println("{}", order);

    const std::string item = order.substr(4, 6);  // a new string: 6 characters from position 4
    std::println("item = {}, first letter = {}, last letter = {}", item, item[0], item.back());

    try {
        std::println("{}", item.at(10));     // at() checks the position; item[10] would not
    } catch (const std::out_of_range&) {
        std::println("at(10) threw std::out_of_range");
    }
    const bool apple_first = std::string("apple") < std::string("banana");
    std::println("{}", apple_first ? "apple sorts first" : "banana sorts first");
}
Appending, inserting, replacing, erasing and slicing Python · editing_session.py
from session import run

run("""
clang++ --version | head -n 1
clang++ -std=c++23 -Wall -Wextra editing.cpp -o editing && ./editing
g++-16 --version | head -n 1
g++-16 -std=c++23 -Wall -Wextra editing.cpp -o editing && ./editing
""")

Output

$ clang++ --version | head -n 1
Apple clang version 21.0.0 (clang-2100.3.34.2)
$ clang++ -std=c++23 -Wall -Wextra editing.cpp -o editing && ./editing
2 x masala dosa, filter coffee (30 characters)
3 x masala dosa
item = masala, first letter = m, last letter = a
at(10) threw std::out_of_range
apple sorts first
$ g++-16 --version | head -n 1
g++-16 (Homebrew GCC 16.2.0) 16.2.0
$ g++-16 -std=c++23 -Wall -Wextra editing.cpp -o editing && ./editing
2 x masala dosa, filter coffee (30 characters)
3 x masala dosa
item = masala, first letter = m, last letter = a
at(10) threw std::out_of_range
apple sorts first

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 editing_session.py

  • += appends, insert(pos, text) inserts before position pos, replace(pos, count, text) replaces count characters, and erase(pos) removes everything from pos to the end.
  • find returns the position of the first match (here the comma), which is how erase knew where to start.
  • substr(pos, count) returns a new string with a copy of those characters.
  • s[i] does not check i: a position past the end is undefined behaviour. s.at(i) checks and throws std::out_of_range, as the output shows. front() and back() give the first and last character.
  • <, == and the other comparisons compare character by character, so "apple" sorts before "banana".

The program also shows that size() counts characters of type char, 30 for the order; the next lesson explains why that is a count of bytes, not of letters, once text is not plain English.

A name normaliser

Real input arrives with stray spaces and mixed case. normalise trims both ends, turns ASCII letters into lower case and collapses each run of spaces and tabs into one space:

normalise.cpp C++
// A name normaliser: trims the ends, lower-cases ASCII letters
// and collapses runs of spaces into one.
#include <cctype>
#include <print>
#include <string>
#include <string_view>

std::string normalise(std::string_view text) {
    std::string out;
    out.reserve(text.size());
    bool pending_space = false;
    for (char c : text) {
        // <cctype> functions need an unsigned char value:
        // passing a negative char is undefined behaviour.
        const auto u = static_cast<unsigned char>(c);
        if (std::isspace(u)) {
            pending_space = !out.empty();  // remember a gap, but never at the start
            continue;
        }
        if (pending_space) out += ' ';
        pending_space = false;
        out += static_cast<char>(std::tolower(u));
    }
    return out;
}

int main() {
    for (std::string_view raw : {"  Aditi   RAO ", "MEERA\tnair", "x", "   "}) {
        std::println("[{}] -> [{}]", raw, normalise(raw));
    }
}
Normalising four inputs Python · normalise_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra normalise.cpp -o normalise && ./normalise
g++-16 -std=c++23 -Wall -Wextra normalise.cpp -o normalise && ./normalise
""")

Output

$ clang++ -std=c++23 -Wall -Wextra normalise.cpp -o normalise && ./normalise
[  Aditi   RAO ] -> [aditi rao]
[MEERA	nair] -> [meera nair]
[x] -> [x]
[   ] -> []
$ g++-16 -std=c++23 -Wall -Wextra normalise.cpp -o normalise && ./normalise
[  Aditi   RAO ] -> [aditi rao]
[MEERA	nair] -> [meera nair]
[x] -> [x]
[   ] -> []

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 normalise_session.py

std::isspace and std::tolower come from C, and they take their argument as an int that must be a value of unsigned char (or EOF); anything else is undefined behaviour, says section 7.4 of the C standard. Every byte of a non-ASCII UTF-8 letter is above 127, and where char is signed, as it is on this Mac, such a byte is stored as a negative char, so the code converts each char to unsigned char first. out.reserve(text.size()) allocates once, since the result is never longer than the input.

Searching: find, contains, starts_with and ends_with

find returns the position of the first match, rfind of the last, and both return std::string::npos, the largest value of std::size_t, when there is no match. C++20 and C++23 added three functions that answer the usual yes-or-no questions directly:

search.cpp C++
// Searching a string: find gives a position or npos;
// contains, starts_with and ends_with give a bool.
#include <print>
#include <string>

int main() {
    const std::string file = "report-q3-final.pdf";

    const auto dash = file.find('-');
    const auto zip = file.find(".zip");
    std::println("first '-' at {}, last '-' at {}", dash, file.rfind('-'));
    std::println("\".zip\" {}", zip == std::string::npos ? "not found (npos)" : "found");

    std::println("contains(\"final\"): {}", file.contains("final"));         // C++23
    std::println("starts_with(\"report\"): {}", file.starts_with("report"));  // C++20
    // C++20, and case matters
    std::println("ends_with(\".PDF\"): {}", file.ends_with(".PDF"));
}
Five searches Python · search_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra search.cpp -o search && ./search
g++-16 -std=c++23 -Wall -Wextra search.cpp -o search && ./search
""")

Output

$ clang++ -std=c++23 -Wall -Wextra search.cpp -o search && ./search
first '-' at 6, last '-' at 9
".zip" not found (npos)
contains("final"): true
starts_with("report"): true
ends_with(".PDF"): false
$ g++-16 -std=c++23 -Wall -Wextra search.cpp -o search && ./search
first '-' at 6, last '-' at 9
".zip" not found (npos)
contains("final"): true
starts_with("report"): true
ends_with(".PDF"): false

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 search_session.py

contains (C++23) replaces s.find(x) != std::string::npos, and starts_with and ends_with (C++20) replace comparisons with substr. All of them compare exactly, so ".PDF" does not match ".pdf".

std::string_view: reading text without copying it

A std::string_view holds two things: a pointer to the first character and a length. A function that takes one can be called with a string literal, a std::string or a view of part of either, and nothing is copied. substr on a view makes another view, so splitting a line into fields allocates nothing. The program below proves it by counting every heap allocation, through its own operator new:

views.cpp C++
// std::string_view refers to characters that someone else owns:
// making a view, or a piece of one, copies nothing.
// This program counts heap allocations to show it.
#include <cstdlib>
#include <new>
#include <print>
#include <string>
#include <string_view>
#include <vector>

static int allocations = 0;

// Every new and delete in the program goes through these two functions, so they can count.
void* operator new(std::size_t size) {
    ++allocations;
    if (void* p = std::malloc(size == 0 ? 1 : size)) return p;
    throw std::bad_alloc{};
}
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }

// The same splitter twice: the first returns copies, the second views into the original text.
std::vector<std::string> split_copies(const std::string& text, char sep) {
    std::vector<std::string> out;
    out.reserve(8);
    for (std::size_t start = 0;;) {
        const auto end = text.find(sep, start);
        out.push_back(text.substr(start, end - start));  // substr allocates when the piece is long
        if (end == std::string::npos) return out;
        start = end + 1;
    }
}

std::vector<std::string_view> split_views(std::string_view text, char sep) {
    std::vector<std::string_view> out;
    out.reserve(8);
    for (std::size_t start = 0;;) {
        const auto end = text.find(sep, start);
        out.push_back(text.substr(start, end - start));  // a string_view's substr never allocates
        if (end == std::string_view::npos) return out;
        start = end + 1;
    }
}

int main() {
    const std::string line = "Mumbai Central to New Delhi,Rajdhani Express number twelve,departs in the evening";

    allocations = 0;
    const auto copies = split_copies(line, ',');
    std::println("{} fields as std::string:      allocations = {}", copies.size(), allocations);

    allocations = 0;
    const auto views = split_views(line, ',');
    std::println("{} fields as std::string_view: allocations = {}", views.size(), allocations);
    std::println("first field: {}", views[0]);
}
Counting heap allocations while splitting a line Python · views_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra views.cpp -o views && ./views
g++-16 -std=c++23 -Wall -Wextra views.cpp -o views && ./views
""")

Output

$ clang++ -std=c++23 -Wall -Wextra views.cpp -o views && ./views
3 fields as std::string:      allocations = 3
3 fields as std::string_view: allocations = 1
first field: Mumbai Central to New Delhi
$ g++-16 -std=c++23 -Wall -Wextra views.cpp -o views && ./views
3 fields as std::string:      allocations = 4
3 fields as std::string_view: allocations = 1
first field: Mumbai Central to New Delhi

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 views_session.py

In both runs one allocation is the vector’s own storage (reserve(8)). With std::string, each field that is too long for the string’s inline buffer allocated too: two with libc++ (Apple clang) and three with libstdc++ (GCC), because “departs in the evening” has 22 characters, which fit inline in libc++ but not in libstdc++. With std::string_view, the fields cost nothing. The C++ Core Guidelines give the same advice (rule SL.str.2): refer to characters you do not own with std::string_view.

A std::string owns its characters on the heap; a std::string_view points into them, and dangles once the string is destroyed.std::string textpointer, size 37,capacitystd::string_view wordpointer, size 8on the heap:"Namaste, and welcometo the C++ track"text destroyed:word now pointsat freed memoryownspoints into

A std::string owns its characters; a std::string_view only points at them

Text description of the diagram

The diagram has four boxes.

  • At the top right is std::string text, which holds a pointer, its size, 37, and its capacity. An arrow labelled owns goes from it to the characters.
  • At the top left is std::string_view word, which holds only a pointer and a size, 8. An arrow labelled points into goes from it to the same characters.
  • In the middle are the characters on the heap: "Namaste, and welcome to the C++ track". The string allocated them and frees them in its destructor.
  • A dashed arrow leads down to the last box: once text is destroyed, word still points at the same place, which is now freed memory.

A view is not a string, and three differences matter:

  • It is read-only: there is no +=, insert or erase.
  • It need not end with a '\0'. view.data() points into someone else’s characters, so passing it to a C function that expects a terminated string can read past the end; the standard says so in a note to data(). Convert with std::string(view) when you need a terminated copy.
  • It does not keep the characters alive. That is the next section.

Small strings live inside the object

The standard does not say how a std::string stores its characters, but both libraries keep short strings inside the std::string object itself, which avoids an allocation; longer strings go on the heap. The limit differs:

sso.cpp C++
// Short strings are stored inside the std::string object itself; longer ones on the heap.
// How short is short depends on the standard library.
#include <cstdint>
#include <print>
#include <string>

// Whether the characters of s sit inside the bytes of the object s.
bool stored_inside(const std::string& s) {
    const auto object = reinterpret_cast<std::uintptr_t>(&s);
    const auto characters = reinterpret_cast<std::uintptr_t>(s.data());
    return characters >= object && characters < object + sizeof s;
}

int main() {
    std::println("sizeof(std::string) = {}, capacity of an empty string = {}", sizeof(std::string),
                 std::string{}.capacity());
    for (std::size_t length : {15, 16, 22, 23}) {
        const std::string s(length, 'x');
        std::println("{} characters: {}", length, stored_inside(s) ? "inside the object" : "on the heap");
    }
}
Where 15, 16, 22 and 23 characters are stored Python · sso_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra sso.cpp -o sso && ./sso
g++-16 -std=c++23 -Wall -Wextra sso.cpp -o sso && ./sso
""")

Output

$ clang++ -std=c++23 -Wall -Wextra sso.cpp -o sso && ./sso
sizeof(std::string) = 24, capacity of an empty string = 22
15 characters: inside the object
16 characters: inside the object
22 characters: inside the object
23 characters: on the heap
$ g++-16 -std=c++23 -Wall -Wextra sso.cpp -o sso && ./sso
sizeof(std::string) = 32, capacity of an empty string = 15
15 characters: inside the object
16 characters: on the heap
22 characters: on the heap
23 characters: on the heap

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sso_session.py

Standard library sizeof(std::string) Characters stored inside
libc++ (Apple clang 21) 24 bytes up to 22
libstdc++ (GCC 16) 32 bytes up to 15

Both are 64-bit builds on the same Mac. This explains the different allocation counts above, and it matters for the next example: a dangling view into a short string may still find the old characters in place, so a test with short text can hide the bug, while text longer than the inline buffer lives in heap memory that AddressSanitizer watches.

A string_view that outlives its string

Here are the two classic ways to create a dangling view. greeting() returns a view into its local string, which is destroyed when the function returns, and temporary.cpp keeps a view of a temporary string, which is destroyed at the end of the statement that made it:

Two dangling views

dangling.cpp

// A std::string_view must not outlive the string it looks at. Both mistakes below compile.
#include <print>
#include <string>
#include <string_view>

std::string_view first_word(std::string_view text) {
    return text.substr(0, text.find(' '));  // fine: a view into the caller's text
}

std::string_view greeting() {
    // longer than the small-string buffer
    const std::string local = "Namaste, and welcome to the C++ track";
    return first_word(local);  // a view into local, which is destroyed when greeting() returns
}

int main() {
    const std::string kept = "Vanakkam everyone";
    std::println("{}", first_word(kept));  // fine: kept is still alive

    const std::string_view word = greeting();
    std::println("{}", word);  // reads the memory of a destroyed string
}

temporary.cpp

// A view of a temporary string: the temporary is destroyed at the end of the line that made it.
#include <print>
#include <string>
#include <string_view>

int main() {
    const std::string city = "Thiruvananthapuram";
    std::string_view shout = city + " Central";  // city + " Central" is a temporary std::string
    std::println("{}", shout);
}
Both mistakes under AddressSanitizer Python · dangling_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined dangling.cpp -o dangling && ./dangling
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined dangling.cpp -o dangling && ./dangling
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined temporary.cpp -o temporary && ./temporary
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined temporary.cpp -o temporary && ./temporary
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined dangling.cpp -o dangling && ./dangling
Vanakkam
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 8 at 0x… thread T0
    #28 in main dangling.cpp:21
0x… is located 0 bytes inside of 40-byte region [0x…,0x…)
freed by thread T0 here:
    #2 in greeting() dangling.cpp:14
    #3 in main dangling.cpp:20
previously allocated by thread T0 here:
    #4 in greeting() dangling.cpp:12
    #5 in main dangling.cpp:20
SUMMARY: AddressSanitizer: heap-use-after-free
[73 more lines of the report left out]
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined dangling.cpp -o dangling && ./dangling
Vanakkam
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 8 at 0x… thread T0
    #8 in main dangling.cpp:21
0x… is located 0 bytes inside of 38-byte region [0x…,0x…)
freed by thread T0 here:
    #5 in greeting() dangling.cpp:14
    #6 in main dangling.cpp:20
previously allocated by thread T0 here:
    #6 in greeting() dangling.cpp:12
    #7 in main dangling.cpp:20
SUMMARY: AddressSanitizer: heap-use-after-free
[58 more lines of the report left out]
[exit status 134]
$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined temporary.cpp -o temporary && ./temporary
temporary.cpp:8:30: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
    8 |     std::string_view shout = city + " Central";  // city + " Central" is a temporary std::string
      |                              ^~~~~~~~~~~~~~~~~
1 warning generated.
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 26 at 0x… thread T0
    #28 in main temporary.cpp:9
0x… is located 0 bytes inside of 32-byte region [0x…,0x…)
freed by thread T0 here:
    #2 in main temporary.cpp:8
previously allocated by thread T0 here:
    #12 in main temporary.cpp:8
SUMMARY: AddressSanitizer: heap-use-after-free
[81 more lines of the report left out]
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined temporary.cpp -o temporary && ./temporary
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 26 at 0x… thread T0
    #8 in main temporary.cpp:9
0x… is located 0 bytes inside of 31-byte region [0x…,0x…)
freed by thread T0 here:
    #5 in main temporary.cpp:8
previously allocated by thread T0 here:
    #7 in main temporary.cpp:8
SUMMARY: AddressSanitizer: heap-use-after-free
[59 more lines of the report left out]
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dangling_session.py

AddressSanitizer stops all four runs with a heap-use-after-free report that names the line that read the memory, the line that freed it and the line that allocated it (the sessions keep those lines of each report and write the addresses, which change on every run, as 0x…). The compilers are less helpful:

  • Clang warns about the temporary (-Wdangling-gsl, on by default); GCC 16 says nothing.
  • Neither compiler warns about the view returned from greeting(), because it passes through first_word, a function that is fine on its own. Clang does warn (-Wreturn-stack-address) when a function returns its local string directly as a view, return local;, but one function call in between is enough to hide it.

So the rules are about lifetimes, not syntax. Return a std::string from a function that builds text; take a std::string_view parameter only for text you read during the call; store a std::string in a variable or a member that must outlive the call; and run tests with -fsanitize=address, using text longer than the inline buffer.

Character Counter Count characters, UTF-8 bytes and lines of any text, to see how long a string really is.

Exercise: split and join

Write split, which cuts a std::string_view at every separator and returns the pieces as views into the original text (keeping empty pieces), and join, which glues pieces together with a separator between them. The checks in main must all print ok; the last one checks that split copies nothing.

split_join.cpp C++
// Exercise: write split and join. Build and run this file: every check must say "ok".
//   split("a,b,c", ',') gives the views {"a", "b", "c"}; empty fields are kept.
//   join({"a", "b", "c"}, ", ") gives the string "a, b, c".
#include <print>
#include <string>
#include <string_view>
#include <vector>

std::vector<std::string_view> split(std::string_view text, char separator) {
    (void)text;
    (void)separator;
    return {};  // replace this
}

std::string join(const std::vector<std::string_view>& parts, std::string_view separator) {
    (void)parts;
    (void)separator;
    return {};  // replace this
}

int failures = 0;

void check(bool passed, std::string_view what) {
    std::println("{}  {}", passed ? "ok    " : "FAILED", what);
    if (!passed) ++failures;
}

int main() {
    using Parts = std::vector<std::string_view>;
    check(split("idli,dosa,vada", ',') == Parts{"idli", "dosa", "vada"}, "three fields");
    check(split("chai", ',') == Parts{"chai"}, "no separator: one field");
    check(split("a,,b", ',') == Parts{"a", "", "b"}, "an empty field in the middle is kept");
    check(join({"idli", "dosa", "vada"}, ", ") == "idli, dosa, vada", "join with a separator");
    check(join({"chai"}, " + ") == "chai", "one part: no separator");

    const std::string line = "Pune;Nashik;Nagpur";
    const auto parts = split(line, ';');
    check(!parts.empty() && parts[0].data() == line.data(),
          "the views point into the original text (no copies)");
    std::println("{} of 6 checks failed", failures);
    return failures == 0 ? 0 : 1;
}

As given, every check fails:

The exercise as given Python · split_join_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined split_join.cpp -o split_join && ./split_join
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined split_join.cpp -o split_join && ./split_join
FAILED  three fields
FAILED  no separator: one field
FAILED  an empty field in the middle is kept
FAILED  join with a separator
FAILED  one part: no separator
FAILED  the views point into the original text (no copies)
6 of 6 checks failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 split_join_session.py

First hint: loop with text.find(separator, start), and stop after the piece for which find returns npos. text.substr(start, end - start) gives the right piece even then, because substr stops at the end of the view.

Self-check

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 s is a long std::string and v is a std::string_view of it. Which expression copies characters?

    Choose one answer.

    Show the answer to question 1

    Answer: s.substr(0, 30)

    std::string::substr returns a new std::string, which copies the characters and allocates when they do not fit inline. A view's substr and a conversion to std::string_view only make a new pointer and length.

  2. Question 2 of 5 Which of these leave a std::string_view that dangles once the statement or function has finished?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • returning std::string_view(local) from a function, where local is a local std::string
    • std::string_view a = std::string("abc") + "def";

    The temporary made by + is destroyed at the end of the statement, and a local string is destroyed when its function returns, so views of them dangle. A string literal lives for the whole program, and a view of s is fine as long as s lives.

  3. Question 3 of 5 What does this print?

    Read the code, then choose one answer.

    const std::string file = "notes.PDF";
    std::println("{} {}", file.ends_with(".pdf"), file.contains("PDF"));
    Show the answer to question 3

    Answer: false true

    The comparisons are exact, so ".pdf" does not match ".PDF", while "PDF" is contained in the name. contains is C++23 and ends_with C++20.

  4. Question 4 of 5 How many characters can a std::string hold inside the object itself with GCC 16's libstdc++, before it allocates?

    Type a number.

    Show the answer to question 4

    Answer: 15

    The recorded run shows 15 for libstdc++ (the object is 32 bytes) and 22 for libc++, Apple clang's library (24 bytes). The standard does not fix either number.

  5. Question 5 of 5 c is a char from UTF-8 text. Which call is correct?

    Choose one answer.

    Show the answer to question 5

    Answer: std::isspace(static_cast<unsigned char>(c))

    The <cctype> functions need a value of unsigned char (or EOF). Bytes of non-ASCII UTF-8 letters are above 127, so a signed char holds them as negative numbers, and passing those is undefined behaviour.

Interview questions

When should a function take std::string_view instead of const std::string&? When it only reads the text during the call. A std::string_view parameter accepts string literals, std::string objects and parts of either without creating a temporary std::string, so a call such as f("literal") allocates nothing, and taking a piece costs nothing. Take const std::string& (or a std::string by value) when the function needs a terminated string for a C API, or keeps the text after it returns.

Key takeaways

  • std::string owns its characters; std::string_view is a pointer and a length into characters owned elsewhere.
  • substr on a std::string copies; on a std::string_view it copies nothing, as the allocation counts show.
  • at() checks positions, [] does not; contains (C++23), starts_with and ends_with (C++20) replace find comparisons.
  • Pass unsigned char values to std::isspace, std::tolower and the other <cctype> functions.
  • Short strings live inside the object: up to 22 characters with libc++, 15 with libstdc++.
  • A view must not outlive its text: never return a view of a local string or keep a view of a temporary; Clang warns about some cases, AddressSanitizer catches them at run time.
  • size() counts char values, which are bytes: the next lesson is about what that means for Unicode text.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.