How C++17 Benefits from Boost Libraries, Part One

Today we have a guest post by Bartlomiej Filipek. Bartek is a C++ programmer, blogger and author. You can find him on LinkedIn or his blog and also read his book.

In today’s article, I’ll show you battle-tested features from the well-known Boost libraries that were adapted into C++17.

With the growing number of elements in the Standard Library, supported by experience from Boost you can write even more fluent C++ code.

Read on and learn about the cool things in C++.

The Series

How C++17 Benefits from Boost Libraries Part One (this post)

How C++17 Benefits from Boost Libraries Part Two

Intro

Some time ago I saw a collection of articles at Fluent C++ about boost algorithms:

In the series, Jonathan described various sorting algorithms, extended partitioning, pattern searching and a few others. I realised that a lot of elements from Boost are now part of the Standard Library, so that inspired me to tackle this topic.

As you know, Boost libraries give us a vast set of handy algorithms, types and features that we don’t have in the Standard Library. Many functionalities were “ported” into core C++. For example, in C++11 we got std::regex , threading and smart pointers.

In that context, we can treat Boost as a testing battleground before moving to the Standard Library.

When I was writing my book about C++17, I’ve noticed that there is a large number of elements that were “moved” from Boost in the new Standard.

For example:

vocabulary types, std::variant , std::any , std::optional

, , string_view

searchers – Boyer Moore and Boyer Moore Horspool

std::filesystem

special math functions

template enhancements

The good news is that if you used only small parts of Boost like boost::variant or boost::optional , now you can use almost the same code and convert to the Standard Library types ( std::variant and std::optional ).

Let’s have a look at those areas, and the first topic is “vocabulary types”.

Vocabulary Types

Being able to write expressive code is a compelling capability. Sometimes using only built-in types doesn’t provide those options. For example, you can set up some number and assign it as “NOT_NUMBER” or treat values of -1 as null entries. As an “ultimate solution” you could even use a pointer and treat nullptr as null… but wouldn’t it be better to have explicit type from the Standard?

Alternatively, how about storing several alternative types in a single object? You can try with C-style unions, but they are hard to use and very low-level… and causing troubles. How about having a type that can store several alternatives… or an object that can store any type?

If you use Boost, then you probably stumbled upon types like boost::optional, boost::variant and boost::any .

Rather than treating -1 as “null number” you leverage optional<int> – if optional is “empty” then you don’t have a number. Simple as it is.

Alternatively, variant<string, int, float> is the type that allows you to store three possible types and switch between them at runtime.

Finally, there’s any that is like a var type in dynamic languages; it can store any type and dynamically change them. It might be int, and later you can switch it to string.

Let’s have a look at some code:

std::optional

The first one is std::optional :

template <typename Map, typename Key> std::optional<typename Map::value_type::second_type> TryFind(const Map& m, const Key& k) { auto it = m.find(k); if (it != m.end()) return std::make_optional(it->second); return std::nullopt; } 1 2 3 4 5 6 7 template < typename Map , typename Key > std :: optional < typename Map :: value_type :: second_type > TryFind ( const Map & m , const Key & k ) { auto it = m . find ( k ) ; if ( it != m . end ( ) ) return std :: make_optional ( it -> second ) ; return std :: nullopt ; }

TryFind returns optional of the value stored in the map, or nullopt . See demo @Wandbox.

You can use it in the following way:

std::map<std::string, int> mm { {"hello", 10}, { "super", 42 }}; auto ov = TryFind(mm, "hello"); // one: std::cout << ov.value_or(0) << '

'; // two: if (ov) std::cout << *ov << '

'; 1 2 3 4 5 6 7 8 9 std :: map < std :: string , int > mm { { "hello" , 10 } , { "super" , 42 } } ; auto ov = TryFind ( mm , "hello" ) ; // one: std :: cout << ov . value_or ( 0 ) << '

' ; // two: if ( ov ) std :: cout << * ov << '

' ;

If the optional ov contains a value, we can access it through the .value() member function or operator* . In the above code, we used another alternative which is the value_or() function that returns the value if present or returns the passed parameter.

std::variant

std::optional stores one value or nothing, so how about storing more types in a safe union type?

Here’s an example:

std::variant<int, float, std::string> TryParseString(std::string_view sv) { // try with float first float fResult = 0.0f; const auto last = sv.data() + sv.size(); const auto res = std::from_chars(sv.data(), last, fResult); if (res.ec != std::errc{} || res.ptr != last) { // if not possible, then just assume it's a string return std::string{sv}; } // no fraction part? then just cast to integer if (static_cast<int>(fResult) == fResult) return static_cast<int>(fResult); return fResult; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 std :: variant < int , float , std :: string > TryParseString ( std :: string_view sv ) { // try with float first float fResult = 0.0f ; const auto last = sv . data ( ) + sv . size ( ) ; const auto res = std :: from_chars ( sv . data ( ) , last , fResult ) ; if ( res . ec != std :: errc { } || res . ptr != last ) { // if not possible, then just assume it's a string return std :: string { sv } ; } // no fraction part? then just cast to integer if ( static_cast < int > ( fResult ) == fResult ) return static_cast < int > ( fResult ) ; return fResult ; }

std::variant can be used to store different types as a parsing result. One common use case is parsing command line or some configuration file. The function TryParseString takes a string view and then tries to parse it into float, int or string. If the floating-point value has no fraction part, then we store it as an integer. Otherwise, it’s float. If the numerical conversion cannot be performed, then the function copies the string.

To access the value stored in a variant, you first have to know the active type. Here’s a code that shows how to do it and use the return value from TryParseString :

const auto var = TryParseString("12345.98"); try { if (std::holds_alternative<int>(var)) std::cout << "parsed as int: " << std::get<int>(var) << '

'; else if (std::holds_alternative<float>(var)) std::cout << "parsed as float: " << std::get<float>(var) << '

'; else if (std::holds_alternative<string>(var)) std::cout << "parsed as string: " << std::get<std::string>(var) << '

'; } catch (std::bad_variant_access&) { std::cout << "bad variant access...

"; } 1 2 3 4 5 6 7 8 9 10 11 12 const auto var = TryParseString ( "12345.98" ) ; try { if ( std :: holds_alternative < int > ( var ) ) std :: cout << "parsed as int: " << std :: get < int > ( var ) << '

' ; else if ( std :: holds_alternative < float > ( var ) ) std :: cout << "parsed as float: " << std :: get < float > ( var ) << '

' ; else if ( std :: holds_alternative < string > ( var ) ) std :: cout << "parsed as string: " << std :: get < std :: string > ( var ) << '

' ; } catch ( std :: bad_variant_access & ) { std :: cout << "bad variant access...

" ; }

The main idea is to use std::holds_alternative() that allows us to check what type is present. variant also offers the .index() member function that returns number from 0… to the max num of stored types.

But one of the coolest uses is a thing called std::visit() .

With this new functionality, you can pass a variant and visit the type that is actively stored. To do it you need to provide a functor that has call operator for all possible types in the given variant:

struct PrintInfo { void operator()(const int& i) const { cout << "parsed as int" << i << '

'; } void operator()(const float& f) const { cout << "parsed as float" << f << '

'; } void operator()(const string& s) const { cout << "parsed as str" << s << '

'; } }; auto PrintVisitorAuto = [](const auto& t) { std::cout << t << '

'; }; const auto var = TryParseString("Hello World"); std::visit(PrintVisitorAuto , var); std::visit(PrintInfo{}, var); 1 2 3 4 5 6 7 8 9 10 struct PrintInfo { void operator ( ) ( const int & i ) const { cout << "parsed as int" << i << '

' ; } void operator ( ) ( const float & f ) const { cout << "parsed as float" << f << '

' ; } void operator ( ) ( const string & s ) const { cout << "parsed as str" << s << '

' ; } } ; auto PrintVisitorAuto = [ ] ( const auto & t ) { std :: cout << t << '

' ; } ; const auto var = TryParseString ( "Hello World" ) ; std :: visit ( PrintVisitorAuto , var ) ; std :: visit ( PrintInfo { } , var ) ;

In the above example, we used two “types” of visitors. The first one – PrintInfo is a structure that provides all overrides for the call operator. We can use it to show more information about the given type and perform unique implementations. The other version – PrintVisitorAuto – leverages generic lambdas, which is convenient if the implementation for all of the types is the same.

You can also read about the overload pattern in a separate blog post. This allows you to write all lambdas locally in a place where std::visit() is called: Bartek’s coding blog: 2 Lines Of Code and 3 C++17 Features – The overload Pattern.

std::any

std::any is probably the least know vocabulary type, and I think there are not many use cases for such a flexible type. It’s almost like var from JavaScript, as it can hold anything.

A little demo of std::any (comes from the proposal N1939):

struct property { property(); property(const std::string &, const std::any &); std::string name; std::any value; }; typedef std::vector<property> properties; 1 2 3 4 5 6 7 struct property { property ( ) ; property ( const std :: string & , const std :: any & ) ; std :: string name ; std :: any value ; } ; typedef std :: vector < property > properties ;

With such property class, you can store any type. Still, if you can restrict the number of possible types, then it’s better to use std::variant as it performs faster than std::any (no extra dynamic memory allocation needed).

More About optional , variant and any

If you want to know more about the vocabulary types you can read separate articles that I wrote on my blog:

std::string_view – non-owning string

std::string_view is a not owning view on the contiguous sequence of characters. It has been ready in Boost for several years now (see boost utils string_view). As far as I know, their interfaces were a bit different, but now the boost version is conformant with C++17.

Conceptually string_view consists of a pointer to the character sequence and the size:

struct BasicCharStringView { char* dataptr; size_t size; }; 1 2 3 4 struct BasicCharStringView { char * dataptr ; size_t size ; } ;

You may wonder what’s unique about std::string_view ?

First of all string_view is a natural replacement for char* arguments. If your function takes const char* and then performs some operation on that, then you can also use view and benefit from nice string-like API.

For example:

size_t CStyle(const char* str, char ch) { auto chptr = strchr(str, ch); if (chptr != nullptr) return strlen(str) + (chptr - str); return strlen(str); } size_t CppStyle(std::string_view sv, char ch) { auto pos = sv.find(ch); if (pos != std::string_view::npos) return sv.length() + pos; return sv.length(); } // use: std::cout << CStyle("Hello World", 'X') << '

'; std::cout << CppStyle("Hello World", 'X') << '

'; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 size_t CStyle ( const char * str , char ch ) { auto chptr = strchr ( str , ch ) ; if ( chptr != nullptr ) return strlen ( str ) + ( chptr - str ) ; return strlen ( str ) ; } size_t CppStyle ( std :: string_view sv , char ch ) { auto pos = sv . find ( ch ) ; if ( pos != std :: string_view :: npos ) return sv . length ( ) + pos ; return sv . length ( ) ; } // use: std :: cout << CStyle ( "Hello World" , 'X' ) << '

' ; std :: cout << CppStyle ( "Hello World" , 'X' ) << '

' ;

See the code @Wandbox

Going further, as you might know, there are many string-like class implementations. CString , QString , etc… and if your code needs to handle many types, string_view might help. Those other types can provide access to the data pointer and the size, and then you can create a string_view object.

Views might also be helpful when doing some work on large strings and when you slice and cut smaller sections. For example, in the parsing of files: You can load file content into a single std::string object and then use views to perform the processing. This might show a nice performance boost as there won’t be any extra copies of strings needed.

It’s also important to remember that since the string_view doesn’t own the data, and also might not be null-terminated, there are some risks associated with using it:

Taking care of the (non)null-terminated strings – string_view may not contain NULL at the end of the string. So you have to be prepared for such a case. Problematic when calling functions like atoi, printf that accepts null-terminated strings

may not contain NULL at the end of the string. So you have to be prepared for such a case. References and Temporary objects – string_view doesn’t own the memory, so you have to be very careful when working with temporary objects. When returning string_view from a function Storing string_view in objects or container.

doesn’t own the memory, so you have to be very careful when working with temporary objects.

A good summary of string views can be found at Marco Arena’s blog post: string_view odi et amo.

starts_with / ends_with New Algorithms

C++20 info: Another good news is that starts_with() / ends_with() algorithms from Boost are now part of C++20… and many compilers already have implemented them. They are available both for string_view and std::string .

Summary

I hope with this blog post I gave you more incentives to start using C++17 :). And this is just the first part of the series!

The last C++ standard offers not only many language features (like if constexpr , structured bindings, fold expressions…), but also a broad set of utilities from the Standard Library. You can now use many vocabulary types: variant , optional , any . Use string views and even a significant component: std::filesystem (see next article). All without the need to reference some external libraries.

This was just the first article in a little series. Please wait for another blog post where I’ll show you more tools that are also available in C++17: std::filesystem , searchers, math functions and more!

Your Turn

What are your favourite features from Boost that you use?

Maybe they will also be merged into the Standard?

Have you ported some boost code into C++17 (and its corresponding feature-set)?

Share your experience in comments.

You will also like

Share this post! Don't want to miss out ?