We will create strings and string views and do basic concatenation and transformation with them in this section:
- As always, we first include header files and declare that we use the std namespace:
#include <iostream>
#include <string>
#include <string_view>
#include <sstream>
#include <algorithm>
using namespace std;
- Let's first create string objects. The most obvious way is instantiating an object a of class string. We control its content by giving the constructor a C-style string (which will be embedded in the binary as a static array containing characters after compiling). The constructor will copy it and make it the content of string object a. Alternatively, instead of initializing it from a C-style string, we can use the string literal operator ""s. It creates a string object on the fly. Using that to construct object b, we can even use automatic type deduction:
int main()
{
string a { "a" };
auto b ( "b"s );
- The strings we just created are copying their input from the constructor argument into their own buffer. In order to not copy, but reference the underlying string, we can use string_view instances. This class does also have a literal operator, and it is called ""sv:
string_view c { "c" };
auto d ( "d"sv );
- Okay, now let's play with our strings and string views. For both types, there are operator<< overloads for the std::ostream class, so they can be printed comfortably:
cout << a << ", " << b << 'n';
cout << c << ", " << d << 'n';
- The string class overloads operator+, so we can add two strings and get their concatenation as a result. This way, "a" + "b" results in "ab". Concatenating a and b this way is easy. With a and c, it is not that easy, because c is not a string, but a string_view. We have to get the string out of c first, and this can be done by constructing a new string from c, and then adding it to a. At this point one could ask, "Wait, why are you copying c into an intermediate string object just in order to add it to a? Can't you avoid that copy by using c.data()?" That is a nice idea, but it has a flaw--string_view instances do not have to carry zero-terminated strings. And this is a problem that can lead to buffer overflows:
cout << a + b << 'n';
cout << a + string{c} << 'n';
- Let's create a new string, which contains all of the strings and string views we just created. By using std::ostringstream, we can print any variable into a stream object that behaves exactly like std::cout, but it doesn't print to the shell. Instead, it prints into a string buffer. After we streamed all the variables with some separating space between them using operator<<, we can construct and print a new string object from that with o.str():
ostringstream o;
o << a << " " << b << " " << c << " " << d;
auto concatenated (o.str());
cout << concatenated << 'n';
- We can now also transform that new string by converting all its letters to upper case, for example. The C library function toupper, which maps lower-case characters to upper-case characters and leaves other characters unchanged, is already available and can be combined with std::transform because a string is basically also an iterable container object with char items:
transform(begin(concatenated), end(concatenated),
begin(concatenated), ::toupper);
cout << concatenated << 'n';
}
- Compiling and running the program leads to the following output, which is just what we expected:
$ ./creating_strings
a, b
c, d
ab
ac
a b c d
A B C D