C++/WinRT: Working with Strings

I think it’s high time I restarted the series on C++/WinRT, providing short “how to” and “how it works” articles to help developers understand some of the fundamental principles of the C++ language projection for the Windows Runtime. As a recap, here’s what I covered thus far:

Consumption and Production

Working with Implementations

Fun with Agility

Optimizing Activation

Working with Namespaces

C++/WinRT in the Windows SDK

cppwinrt.exe in the Windows SDK

C++/WinRT now ships in the Windows SDK and the cppwinrt compiler is also available. Visual Studio 2017 even provides tentative support, so there’s a lot to talk about. The original Getting Started assumed that you had to get the C++/WinRT headers from GitHub. That’s no longer the case, so let’s get started again but this time with Visual Studio and go from there. This guide will assume that you have installed Visual Studio 2017 15.6 or later as well as the Windows SDK for RS4 or later. The 15.6 update of Visual Studio 2017 provides command line support for C++/WinRT. While the Window SDK will ensure that cppwinrt.exe is available from a developer command prompt, it’s Visual Studio that ensures that the headers are in the include path.

The C++ language has a bad reputation when it comes to strings. Unlike many other languages, C++ doesn’t have a built-in type representing a string of text. Rather, the C++ library fills that gap by providing std::string and its variants. Inevitably, std::string doesn’t work for everyone and thus we land up with a myriad of string types to contend with. C++17 attempts to alleviate some of this pain with the introduction of string conversion utilities and the loveable but contentious std::string_view class. Of course, if that were the end of the story it would certainly have a happy ending.

Sadly, std::string is just one option. There’s also std::wstring, largely for Windows developers where wchar_t dominates. The trouble is that wchar_t is 2 bytes on Windows and represents UTF-16 characters whereas wchar_t is typically 4 bytes on most other platforms and represents UTF-32 characters. Then there’s the more explicit std::u16string and std::u32string. And on it goes.

So what’s the solution? We need another string type! Seriously, the idea of a universal string type for C++ is unrealistic today. Perhaps one day we’ll get there. Until then, we have a partial solution and that is the std::string_view to bridge the gap between different libraries and their unique requirements. While C++/WinRT has its own string type, it provides convertibility with std::wstring_view specifically to address this problem.

C++/WinRT requires its own string type because while WinRT uses wchar_t characters, the ABI itself is not a subset of what either std::wstring or std::wstring_view provides and using those would lead to significant inefficiency. Instead, C++/WinRT provides the winrt::hstring that represents an immutable string consistent with the underlying HSTRING, but in such a way that C++ developers can largely ignore the specifics of WinRT string management and just work with what they know and love about C++. Consider the following example:

using namespace Windows::Networking::Connectivity;

int main()
{
    for (auto&& host : NetworkInformation::GetHostNames())
    {
        hstring name = host.DisplayName();

        printf("%ls\n", name.c_str());
    }
}

GetHostNames returns a collection of HostName objects representing the various network names associated with the computer. I can then call DisplayName to get a string representation. Since DisplayName is a WinRT API it naturally returns an hstring, but notice that I could just as easily have used auto:

auto name = host.DisplayName();

printf("%ls\n", name.c_str());

Suddenly, this looks very much like std::wstring. Indeed, there’s no reason to have a named local. I can more concisely express it as follows:

printf("%ls\n", host.DisplayName().c_str());

As you can see, it looks and feels like std::wstring and provides many of the same type aliases and functions. We could have changed the cppwinrt compiler to generate API strings using std::wstring directly, but that would have been inefficient. Still, we aim to make it as transparent as possible. You can for example, pass an hstring to a function expecting a std::wstring_view:

void print(std::wstring_view text);

int main()
{
    for (auto&& host : NetworkInformation::GetHostNames())
    {
        print(host.DisplayName());
    }
}

Notice that I’m not calling c_str or any other conversion helper. Since the hstring is convertible to std::wstring_view – and at no cost – this just works. It works the other way as well. I can create a HostName object directly, perhaps in anticipation of creating a socket connection:

using namespace Windows::Networking;

int main()
{
    HostName host(L"moderncpp.com");
}

While the HostHame constructor technically expects an hstring, it’s quite happy if you pass it a string literal. In fact, this is often more efficient than creating an hstring yourself. You might instead have a std::wstring or just a std::wstring_view and that will work just as well:

int main()
{
    std::wstring name = L"moderncpp.com";

    HostName host(name);
}

What about a std::wstring_view literal? No problem:

using namespace std::literals;

int main()
{
    HostName host(L"moderncpp.com"sv);
}

If you’re really curious, you’ll notice that all of the input parameters that should logically accept a winrt::hstring really expect a winrt::param::hstring. The param namespace has a set of types that are used exclusively for optimizing input parameters to naturally bind to STL types and avoid copies as well as other inefficiencies. You should never use those types directly, but that’s where the magic happens to make this work efficiently for input. Again, you should never use those param types yourself. Don’t use them as an optimization for your own functions – just use std::wstring_view.

The hstring also provides all of the comparison operators so that you can naturally and efficiently compare against its counterparts in the C++ standard library. It also includes everything you need to use hstring as a key for associative containers if need be. We also recognize that many C++ libraries use std::string and work exclusively with UTF-8 text. As a convenience, we provide helpers for converting back and forth as well:

hstring w = L"hello world";

std::string c = to_string(w);
assert(c == "hello world");

w = to_hstring(c);
assert(w == L"hello world");

As a team, we spend a lot of time focusing on performance. That includes machine instructions, but it also includes binary size. All of that comes into play when we talk about strings because of how heavily strings are used in the Windows Runtime. There are many other optimizations and affordances we provide for strings, but hopefully this gives you a good idea of how to make use of strings with C++/WinRT.