String Views

A practical introduction to string views, and why they should be the main way we pass strings to functions
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

So far, we’ve learned that a range of different options exist for representing strings. We’d ideally like to use just one type through our whole application, but that’s not always possible.

In complex projects, we’ll be using third-party libraries and operating system APIs that use different string types. We need to bring that all together to create a cohesive application, so we need a way to work with different string types in a somewhat organized way.

C++17 introduced string views, which are a useful tool that can help us achieve this.

Why do we need String Views?

The main problem string views intend to solve crops up when we’re dealing with string-based function arguments. Here, we’re passing a C-style string into a function that accepts a std::string:

#include <iostream>

void HandleString(const std::string& Input){
  std::cout << Input;
}

int main(){
  HandleString("Hello World");
}

As we’ve seen before, std::string objects can be constructed from C-style strings, so our code works as expected:

Hello World

However, even though our parameter type is a reference, our function is being called with a different data type entirely. Creating a std::string& parameter from a char* argument requires the argument to be copied, just as if we were passing it by value.

This is particularly problematic with strings as, under the hood, strings are just collections of characters. And like any other collection, creating a deep copy means every object in the collection needs to be copied. That’s bad for performance and gets worse the bigger the strings are.

A string view lets us circumvent the need to create a copy in scenarios like this. It provides a read-only interface for an underlying string.

String views work across many string types, giving us a consistent set of methods to work with the strings, without the performance hit of converting the strings to a consistent type.

Creating String Views

The standard library’s implementation of a string view is std::string_view, available by including <string_view>

#include <string_view>

std::string_view Input;

std::string_view literals use double quotes, with the sv suffix. They are available after adding a using statement for the std::string_view_literals namespace:

#include <string_view>

using namespace std::string_view_literals;

std::string_view Input{"Hello"sv};

Creating String Views from Strings

std::string_view objects can be implicitly created from most built-in string types, including std::string and C-style strings. They can also be created from other std::string_view objects:

#include <iostream>
#include <string_view>

void HandleString(std::string_view Input){
  std::cout << Input;
}

int main(){
  using namespace std::literals;
  HandleString("This was a C-style string");
  HandleString("\nThis was a std::string"s);
  HandleString("\nThis was a string_view"sv);
}
This was a C-style string
This was a std::string
This was a string_view

Creating String Views from Iterators

String views can also be constructed from a pair of iterators. This is most commonly used when we want our view to only have access to part of the underlying string:

#include <iostream>
#include <string_view>

void Log(std::string_view Input){
  std::cout << Input << '\n';
}

int main(){
  std::string Hello{"Hello World"};
  Log({Hello.begin(), Hello.end()}); 
  Log({Hello.begin(), Hello.begin() + 5}); 
  Log({Hello.end() - 5, Hello.end()}); 
}
Hello World
Hello
World

"Wide" String Views

Strings are collections of characters and, as we covered in our earlier lessons, the specific type of character is something we may want to change.

Just as std::string is an alias for std::basic_string<char>, so too is std::string_view an alias for std::basic_string_view<char>

We can use different character types by changing the template parameter. Below, we create a string view that uses wchar_t objects, which are 16-bit characters:

#include <iostream>

int main(){
  using namespace std::string_literals;
  std::basic_string_view<wchar_t> A{L"Hello"};

  // std::basic_string_view<wchar_t>
  // is aliased to std::wstring_view:
  std::wstring_view B{L"World"};

  std::wcout << A << ' ' << B;
}
Hello World

We covered character types, and their implications, in more detail earlier in this chapter:

Dangling String Views

A string view is little more than a lightweight pointer to a string managed by a different object entirely, Because of this, we must ensure that this underlying string object isn't deallocated when our view still needs it.

Similar to a dangling pointer, a view will become dangling when the string it was viewing no longer exists.

Trying to use a dangling view results in undefined behavior. So, in long-running programs, we need to be mindful of our object lifecycles.

However, a common form of error involves creating a view that becomes dangling immediately, due to improper initialization. This happens when we initialize a string view with a value that is going to be cleaned up as soon as the expression ends.

The following code shows two examples of this, where we create std::string objects that will be deallocated immediately after our views are created, leaving both of them dangling:

#include <string_view>

std::string GetString(){
  return {"Hello"};
}

int main(){
  std::string_view A{GetString()}; 

  using namespace std::string_literals;
  std::string_view B{"World"s}; 
}

C-style string literals and string view literals have static duration, so they are the recommended way to create string views of a static value:

#include <string_view>

int main(){
  std::string_view A{"Hello"}; 

  using namespace std::string_view_literals;
  std::string_view B{"World"sv}; 
}

const and constexpr String Views

Whilst the underlying string cannot be edited through a view, there are some ways the view itself can be changed. For example, we can reassign the view to point to a different string:

#include <string_view>

int main(){
  std::string A {"Hello"};
  std::string B {"World"};
  std::string_view View{A};
  View = B;
}

The next lesson will also introduce ways we can edit the view such that it has visibility of just part of the underlying string.

If we want to prevent these edits to a string view, we can mark it as const in the usual way:

#include <string_view>

int main() {
  std::string A {"Hello"};
  std::string B {"World"};
  const std::string_view View{A};
  View = B;// Error - cannot reassign const
}

Additionally, string views can be marked as constexpr. constexpr string views do not have the same limitations as constexpr std::string objects.

As a result, using a string_view is the recommended way of having a constexpr string in modern C++

constexpr std::string_view Name{"Bob"};

Creating std::string objects from String Views

A std::string can easily be created from a std::string_view. However, this requires an expensive character-by-character copy, so the compiler will not allow it to be done implicitly:

#include <iostream>
#include <string_view>

void HandleString(const std::string& Input){
  std::cout << Input;
}

int main(){
  using namespace std::string_view_literals;
  HandleString("Hello World"sv);
}
error: cannot convert argument 1 from 'std::string_view' to 'const std::string &'

It can be done explicitly, by directly calling the std::string constructor, or using static_cast:

#include <iostream>
#include <string_view>

void HandleString(const std::string& Input){
  std::cout << Input;
}

int main(){
  using namespace std::string_view_literals;
  HandleString(std::string("Hello"sv));

  HandleString(static_cast<std::string>(
    " World"sv
  ));
}
Hello World

Creating C-Style strings from String Views

Creating a C-style string from a string view requires a little more thought. The data() method on the string view returns a const char*

#include <string_view>
#include <iostream>

void HandleString(const char* Input){
  std::cout << Input;
}

int main(){
  using namespace std::string_view_literals;
  std::string_view View{"Hello World"sv};
  HandleString(View.data()); 
}
Hello World

However, we may not be able to use this as a C-style string, because the string that the view was associated with may not have been null-terminated.

If the underlying string isn’t null-terminated (or we’re not sure), we should recreate the string to make sure.

Typically, this is done by generating a std::string from the string view, and then using its c_str() method:

#include <iostream>
#include <string_view>

void HandleString(const char* Input){
  std::cout << Input;
}

int main(){
  using namespace std::string_view_literals;
  std::string_view View{"Hello World"sv};

  HandleString(std::string(View).c_str());
}
Hello World

std::string_view vs const std::string&

It seems there’s not much difference between a std::string_view and a constant std::string reference. So, when we have a std::string, and our function needs read-only access to it, which should we use?

When our argument is specifically a std::string, there isn’t much difference. Both std::string_view and const std::string& will result in a read-only parameter, and both types can be initialized very efficiently from a std::string, as no copying is needed.

However, we should still generally prefer to use a std::string_view. Even if we know our function is only called with std::string objects right now, things may change in the future.

And, as soon as someone calls our function with something like a char*, the const std::string& parameter type requires a copy, degrading performance. A std::string_view parameter would have handled the different type with just the same efficiency.

Where to avoid using String Views

Despite their desirable traits, there are a few scenarios where we shouldn’t be using string views as our function parameters.

The first scenario is when we need write access to the string. String views are read-only, so we can't use them for that scenario. Depending on our specific requirements, we need to pass the argument by value, or by a non-const reference. Our parameter will then need to be a specific string type to copy into, or a specific reference type respectively.

The second scenario where we want to avoid string views is where our function is passing the argument off to another function, and where that function expects a specific string type.

Scenarios like this will typically involve us trying to recreate a concrete string type using our string view.

If we find ourselves doing that too often, we’re probably misusing string views and would be better served by just using traditional reference types instead.

Summary

In this lesson, we explored std::string_view demonstrating its role in optimizing string handling by avoiding unnecessary copies while maintaining a consistent interface across different string types.

Main Points Learned:

  • The introduction of std::string_view in C++17 as a tool for efficient string manipulation.
  • How std::string_view provides a read-only view over strings, avoiding the performance cost of copying.
  • Creation and usage of std::string_view from C-style strings, std::string, and through literals.
  • The ability to construct string views from a range using iterators for partial views.
  • The concept of wide string views with wchar_t and the usage of std::wstring_view.
  • The importance of avoiding dangling string views and ensuring the lifetime of the underlying string.
  • How to handle string views in functions, including converting to std::string explicitly when necessary.
  • The comparison between using std::string_view and const std::string& for function parameters to enhance performance.
  • When it is appropriate and when to avoid using std::string_view based on the need for write access or a specific string type.

Was this lesson useful?

Next Lesson

Working with String Views

An in-depth guide to std::string_view, including their methods, operators, and how to use them with standard library algorithms
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Next Lesson

Working with String Views

An in-depth guide to std::string_view, including their methods, operators, and how to use them with standard library algorithms
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved