Standard Library Utilities in C++

fenbf
63.2K views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

C++17 features, stl utils

Table Of Contents:

What I like about C++17 is that it finally brings a lot of features and patterns that are well known but come from other libraries. For example, for years programmers have been using boost libraries. Now, many of boost sub -libraries are merged into the standard. That merging process makes the transition to the modern C++ much easier, as most of the time the code will just compile and work as expected. Not to mention is the fact that soon you won't need any third party libraries.

Let's have a look at the following features:

  • std::any - adapted from boost any
  • std::variant - and the corresponding boost variant
  • std::optional - boost optional library
  • std::string_view
  • Searchers for std::search
  • Plus a few other mentions

This playground is adapted from my blog: Bartek's coding blog: C++17 in details: Standard Library Utilities.

Visit the blog if you're looking for more good stuff about C++ :)

Documents & Links

First of all, if you want to dig into the standard on your own, you can read the latest draft here:

N4659, 2017-03-21, Draft, Standard for Programming Language C++ - from isocpp.org.

Also, you can grab my list of concise descriptions of all of the C++17 - It's a one-page reference card, pdf language features:

Grab it here: C++17 Reference Card

Links:

And the books:

OK, let's discuss the utils!

Library Fundamentals V1 TS and more

Most of the utilities described today (std::optional,std::any, std::string_view, searchers) comes from so called "Library Fundamentals V1". It was in Technical Specification for some time, and with the paper "P0220R1 - Adopt Library Fundamentals V1 TS Components for C++17 (R1") it got merged into the standard.

Support:

When I describe the features, I write "compiler" support, but when discussing library features, I should mention the library implementation. For the sake of simplification, I'll just stick to compiler name as each common compiler (GCC, Clang, MSVC) have its separate libs.

And now the features:

std::any

A better way to handle any type and replace void*.

Node from n4562:

The discriminated type may contain values of different types but does not attempt conversion between them, i.e. 5 is held strictly as an int and is not implicitly convertible either to "5" or to 5.0. This indifference to interpretation but awareness of type effectively allows safe, generic containers of single values, with no scope for surprises from ambiguous conversions.

In short, you can assign any value to existing any object:

auto a = std::any(12);
a = std::string("hello world");
a = 10.0f;

When you want to read a value you have to perform a proper cast:

auto a = std::any(12);
std::cout << std::any_cast<int>(a) << '\n'; 

try 
{
    std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e) 
{
    std::cout << e.what() << '\n';
}

Here's a bigger runnable sample (GCC 7.1):

std::any sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <string>
#include <iostream>
#include <any>
#include <map>
int main()
{
auto a = std::any(12);
// set any value:
a = std::string("Hello!");
a = 16;
// reading a value:
// we can read it as int
std::cout << std::any_cast<int>(a) << '\n';
// but not as string:
try
{
std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
// reset and check if it contains any value:
a.reset();
if (!a.has_value())
{
std::cout << "a is empty!" << "\n";
}
// you can use it in a container:
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;
for (auto &[key, val] : m)
{
if (val.type() == typeid(int))
std::cout << "int: " << std::any_cast<int>(val) << "\n";
else if (val.type() == typeid(std::string))
std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
else if (val.type() == typeid(float))
std::cout << "float: " << std::any_cast<float>(val) << "\n";
}
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Notes

  • any object might be empty.
  • any shouldn't use any dynamically allocated memory, but it's not guaranteed by the spec.

More info in:

MSVC VS 2017, GCC: 7.0, Clang: 4.0

std::variant

Type safe unions!

With a regular union you can only use POD types (correction: since C++11 it's possible, assuming you provide required operation like a copy constructor, move... see union declaration)), and it's not safe - for instance, it won't tell you which variant is currently used. With std::variant it's only possible to access types that are declared.

For example:

std::variant<int, float, std::string> abc;

abc can only be initialized with int, float or string and nothing else. You'll get a compile time error when you try to assign something else.

To access the data, you can use:

  • std::get with index or type of the alternative. It throws std::bad_variant_access on errors.
  • std::get_if - returns a pointer to the element or nullptr;
  • or use std::visit method that has usage especially for containers with variants.

A bigger playground (GCC 7.1):

std::variant sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <string>
#include <iostream>
#include <variant>
struct SampleVisitor
{
void operator()(const int &i) const { std::cout << "int: " << i << "\n"; }
void operator()(const float& f) const { std::cout << "float: " << f << "\n"; }
};
int main()
{
std::variant<int, float> intOrFloat;
static_assert(std::variant_size_v<decltype(intOrFloat)> == 2);
// default initialized to the first alternative, should be 0
std::visit(SampleVisitor(), intOrFloat);
// won't compile:
// error: no match for 'operator=' (operand types are 'std::variant<int, float>' and 'std::__cxx11::basic_string<char>')
// intOrFloat = std::string("hello");
// index will show the currently used 'type'
std::cout << "index = " << intOrFloat.index() << std::endl;
intOrFloat = 100.0f;
std::cout << "index = " << intOrFloat.index() << std::endl;
// try with get_if:
if (const auto intPtr (std::get_if<int>(&intOrFloat)); intPtr)
std::cout << "int!" << *intPtr << "\n";
else if (const auto floatPtr (std::get_if<float>(&intOrFloat)); floatPtr)
std::cout << "float!" << *floatPtr << "\n";
// visit:
std::visit(SampleVisitor(), intOrFloat);
intOrFloat = 10;
std::visit(SampleVisitor(), intOrFloat);
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Notes:

  • Variant is not allowed to allocate additional (dynamic) memory.
  • A variant is not permitted to hold references, arrays, or the type void.
  • The first alternative must always be default constructible
  • A variant is default initialized with the value of its first alternative.
  • If the first alternative type is not default constructible, then the variant must use std::monostate as the first alternative

More info:

MSVC VS 2017, GCC: 7.0, Clang: 4.0?

std::optional

Another and elegant way to return objects from functions that are allowed to be empty.

For example:

std::optional<std::string> ostr = GetUserResponse();

if (ostr)
    ProcessResponse(*ostr);
else
    Report("please enter a valid value");

In the simple sample above GetUserResponse returns optional with a possible string inside. If a user doesn't enter a valid value ostr will be empty. It's much nicer and expressive than using exceptions, nulls, output params or other ways of handling empty values.

A better example (GCC 7.1):

std::optional sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <optional>
#include <iostream>
#include <string>
std::optional<int> GetInt(int r)
{
if (r % 2 == 0)
return r/2;
return { };
}
void ShowOptionalInt(const std::optional<int>& oi)
{
if (oi)
std::cout << "int ok: " << *oi << "\n";
else
std::cout << "bad int...\n";
}
int main()
{
std::cout << sizeof(int) << ", " << sizeof(std::optional<int>) << "\n";
std::cout << sizeof(double) << ", " << sizeof(std::optional<double>) << "\n";
std::cout << sizeof(std::string) << ", " << sizeof(std::optional<std::string>) << "\n";
auto oi = GetInt(10);
ShowOptionalInt(oi);
auto oi2 = GetInt(11);
ShowOptionalInt(oi2);
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Notes:

  • Implementations are not permitted to use additional storage, such as dynamic memory, to allocate its contained value. The contained value shall be allocated in a region of the optional storage suitably aligned for the type T.

More info:

MSVC VS 2017, GCC: 7.0, Clang: 4.0?

string_view

Although passing strings got much faster with move semantics from C++11, there's still a lot of possibilities to end up with many temporary copies.

A much better pattern to solve the problem is to use a string view. As the name suggests instead of using the original string, you'll only get a non-owning view of it. Most of the time it will be a pointer to the internal buffer and the length. You can pass it around and use most of the common string functions to manipulate.

Views work well with string operations like sub string. In a typical case, each substring operation creates another, smaller copy of some part of the string. With string view, substr will only map a different portion of the original buffer, without additional memory usage, or dynamic allocation.

Another important reason for using views is the consistency: what if you use other implementations for strings? Not all devs have the luxury to work only with the standard strings. With views, you can just write (or use) existing conversion code, and then string view should handle other strings in the same way.

In theory string_view is a natural replacement for most of const std::string&.

Still, it's important to remember that it's only a non-owning view, so if the original object is gone, the view becomes rubbish.

If you need a real string, there's a separate constructor for std::string that accepts a string_view. For instance, the filesystem library was adapted to handle string view (as input when creating a path object).

Ok, but let's play with the code (GCC 7.1):

std::string_view sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <string>
#include <iostream>
#include <string_view>
// we need to overload 'new' to see what's
// hapenning under the hood...
void* operator new(std::size_t n)
{
std::cout << "new " << n << " bytes\n";
return malloc(n);
}
int main()
{
// the original string, one allocation:
std::string str {"Hello Amazing Programming World" };
// another allocation for the substring, separate 'copy'
auto subStr = str.substr(str.find("Programming"));
std::cout << subStr << "\n";
// no allocation for the sub range:
std::string_view strView { str };
auto subView = strView.substr(str.find("Programming"));
std::cout << subView << "\n";
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

More info:

MSVC VS 2017, GCC: 7.0, Clang: 4.0?

Searchers

When you want to find one object in a string, you can just use find or some other alternative. But the task complicates when there's a need to search for a pattern (or a sub range) in a string.

The naive approach might be O(n*m) (where n is the length of the whole string, m is the length of the pattern).

But there are much better alternatives. For example Boyer-Moore with the complexity of O(n+m).

C++17 updated std::search algorithm in two ways:

  • you can now use execution policy to run the default version of the algorithm but in a parallel way.
  • you can provide a Searcher object that handles the search.

For now we have three searchers:

  • default_searcher
  • boyer_moore_searcher
  • boyer_moore_horspool_searcher

You can play with the example here (GCC 7.1):

searchers sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
// in gcc 7.1 still have to use experimental namespace/headers
#include <experimental/functional>
#include <experimental/algorithm>
#include <chrono>
template <typename TFunc> void RunAndMeasure(TFunc func)
{
const auto start = std::chrono::steady_clock::now();
func();
const auto end = std::chrono::steady_clock::now();
std::cout << std::chrono::duration <double, std::milli> (end - start).count() << " ms\n";
}
int main()
{
std::string in;
std::string needle = "Hello Amazing Programming World";
RunAndMeasure([&]() {
const int maxIters = 20000000u;
for (int i = 0; i < maxIters; ++i)
{
in += "abcd ";
if (i % 7)
in += "xyz, uvw ";
if (i == maxIters - 100)
in += " $" + needle + "$ ";
}
});
RunAndMeasure([&]() {
auto it = std::experimental::search(in.begin(), in.end(),
std::experimental::default_searcher(
needle.begin(), needle.end()));
if(it == in.end())
std::cout << "The string " << needle << " not found\n";
});
RunAndMeasure([&]() {
auto it = std::experimental::search(in.begin(), in.end(),
std::experimental::boyer_moore_searcher(
needle.begin(), needle.end()));
if(it == in.end())
std::cout << "The string " << needle << " not found\n";
});
RunAndMeasure([&]() {
auto it = std::experimental::search(in.begin(), in.end(),
std::experimental::boyer_moore_horspool_searcher(
needle.begin(), needle.end()));
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Which version is the fastest?
  • Is this better than just std::string::find?

More info:

MSVC VS 2017.3, GCC: 7.0, Clang: 3.9?

Other Changes

  • shared_ptr with array - P0414R2: Merging shared_ptr changes from Library Fundamentals to C++17. So far unique_ptr was able to handle arrays. Now it's also possible to use shared_ptr.
  • Splicing Maps and Sets - PDF P0083R2 - we can now move nodes from one tree based container (maps/sets) into other ones, without additional memory overhead/allocation.
  • Mathematical special functions - PDF: P0226R1
  • Improving std::pair and std::tuple - N4387) - pair/tuple obey the same initialization rules as their underlying element types.
  • Sampling - n4562: Sampling - new algorithm that selects n elements from the sequence
  • Elementary string conversions - P0067R5, new function to_chars that handles basic conversions, no need to use stringstream, sscanf, itoa or other stuff.

Summary

Did I miss something? Yes!

There are many other changes in STL that would fill another post (or I could expand the "Other Changes" section). But let's stop for now. Note that each of those 'small' utils are worth a separate post, with more example, so I'll definitely plan to do that later :)

If you want to dig deeper try to read the spec/draft or look at the official paper with changes: P0636r0: Changes between C++14 and C++17 DIS.

As I mentioned, I like that C++17 merged many useful well-known patterns into STL. There's a high chance you've come across many of the features and using them in a project shouldn't be that hard.

What do I like the most?

I think:

  • Filesystem - a significant portion of the library, that will make code much easier and common across many platforms.
  • type safe helpers: std::any, std::optional, std::variant - we can now replace void* or C style unions. The code should be safer.
  • string features: like string_view, string conversions, searchers.
  • parallelism - very powerful abstraction for threading.

Still, there's a lot of stuff to learn/teach! I've just described the features, but the another part of the equation is to use them effectively. And that needs experience.

  • What are your favourite features from C++17 STL?
  • What have I missed? What else should be in my C++17 posts?
  • Have you already used any/optional/variant, for example from boost?
Open Source Your Knowledge: become a Contributor and help others learn. Create New Content