What's new in Modern C++ (C++17) Part II?

Markus Spiske from upsplash
Markus Spiske from upsplash

As promised in the previous blog post, there are many more features for C++17. This time around, we will look at some others new notable features.

Let's continue our discovery of C++17 features;

string_view

This is an unfortunate side-effect of C++ being interoperable with C language where we need to accept an old-school list of characters in the format of const char *.

In old C++, we probably need to do something similar to this;

Bar getBar(const string& s);
Bar getBar(const char* s);
Bar getBar(const char* s, int length);

In new C++, this got streamlined and it becomes much much simpler;

Bar getBar(string_view str);

This is not new by any means, it has been done before by the introduction of boost::string_ref, QStringRef etc. string_view is a standardized version of those ✨.

How to use it? I think you can just throw string_view in every place that we used to do const std::string&. It should work like a charm.

Optional

Let's say we have a need to design an API that needs to communicate with user for a failure case.

Bar getBar(string_view str);

Maybe using int or bool to communicate back to our user of this API? (using -1 and false to indicate error? 😶)

int getBar(Foo f, Bar& bar); //💩
bool getBar(Foo f, Bar& bar); //💢

Or slightly better use pointer to communicate this back to the user?

//return null on failure
unique_ptr<Bar> getBar(Foo f);

Both abovementioned options are not nice, don't we have a better tool for this? Answer: Yes, we do. Introducing optional.

optional<Bar> getBar(Foo f);

How do we use this function?

auto bar = getBar(f);
if (bar) {
  //do something with a real value, access real value by using `*bar`
}

or nicer with one-liner.

if (auto bar = getBar(f); bar) {
 cout << *bar;
} else {
 //if bar is null (`std::nullopt`), we can do some more thing in here.
}

This makes our interface nicer to work with.

Variant & Any

Let's say we have a need to represent data that is dynamically changed at runtime. (I am looking at you JSON 👀).

JSON's value could be the following; a string, a number, an object (JSON object), an array, a boolean !

Let's do that with old C++;

struct JsonValue {
  union Value {
    int n,
    string s,
    JsonValue j,
    vector<JsonValue> arr,
    bool b,
  };
  // you need a type to let the call-site know what type it represents
  enum class Type {
    INT,
    STRING,
    JSON,
    ARRAY,
    BOOL
  };
  
  Value value;
  Type type;
}

With C++17, this is magically represented by using variant.

struct JsonValue {
  variant<int, string, JsonValue, vector<JsonValue>, bool> value;
}

How to use? visit is a way to go! We can use either lambda or struct like this;

struct JsonVisitor {
  void operator()(int i) const { 
    //do something when it is `int`
  }
  void operator()(string s) const {
    // it is `string`
  }
  void operator()(bool b) const {
    // it is `bool`
  }
};
  

variant<int, string, JsonValue, vector<JsonValue>, bool> value;
  
value = 30;
visit(JsonVisitor{}, value); //<-- handled by JsonVisitor::operator()(int i)
  
value = "string";
visit(JsonVisitor{}, value); //<-- JsonVisitor::operator()(string s)

How about Any? While variant creates a contract that it promises to hold just only limited types, with any, it can hold pretty much anything!!

 any v = createValue();
 
 if (v.type() == typeid(int)) {
   auto i = any_cast<int>(v);
 }

Or you can do some fancy stuff like this

//things can be multiple objects ... 
vector<Thing*> v; //instead of using this 🤔

vector<any> v; //<- you may use this which can hold anything 😯

Filesystem

Filesystem is C++ before C++17 is a wild wild west. POSIX and Windows implementation are quite different. This leads to an introduction of preprocessing macros like _WIN32, __APPLE__ or __posix. The maintenance for these branching is suboptimal and giving us headache. 😢

Fear no more, in C++17 with introduction of filesystem namespace std::filesystem, we are having a consistent implementation.

For listing, files and/or folders in directory, one might do

void printContentOf(const filesystem::path& path) {
  for (auto const& item : filesystem::director_iterator(p)) {
    auto status = item.status();
    if (filesystem::is_regular_file(status) {
      cout << "File : " << item.path().filename() << "\n";
    } else if (filesystem::is_directory(status)) {
      cout << "Dir : " << item.path().filename() << "\n";
    }
  }
}

No more include windows.h, no more using struct dirent, only joy 😄...

As you can see, I think C++17 is a big step into the right direction for C++. It makes the new code nicer and easier to write, yet maintain the top-notch preformance. If you haven't checked C++ for a while, I suggest you try to do it now. C++ has grown and improve a lot, it might give you a bit of surprise. 🎉