Last time, we learned about the sadness of treating counted strings as null-terminated strings. How can we escape this cycle of depression?
Almost all of the counted-string types have a way of creating an instance with a pointer and a count. The trick is to use it.
| Class | Creator |
|---|---|
std::wstring |
std::wstring(p, n) |
HSTRING |
WindowsCreateString(p, n, &s) |
Platform::String |
ref new String(p, n) |
winrt::hstring |
winrt::hstring(p, n) |
BSTR |
SysAllocStringLen(p, n) |
_bstr_t |
_bstr_t(SysAllocStringLen(p, n), FALSE) |
CComBSTR |
CComBSTR(n, p) |
CStringW |
CStringW(p, n) |
In most cases, the creator is fairly obvious. The one weirdo is _bstr_t, which has no built-in constructor for counted strings. Instead, you have to use the underlying SysÂAllocÂStringÂLen function to create the counted string, and then pass it to the constructor in attach mode (bCopy = FALSE).
Of course, once you get the counted string in, you need a way to get it back out.
| Class | Pointer and length |
|---|---|
std::wstring |
s.c_str(), s.size() |
HSTRING |
WindowsGetStringRawBuffer(s, &n), n |
Platform::String |
s->Data(), s->Length() |
winrt::hstring |
s.c_str(), s.size() |
BSTR |
s, SysStringLen(s) |
_bstr_t |
static_cast<wchar_t*>(s), s.length() |
CComBSTR |
static_cast<BSTR>(s), s.Length() |
CStringW |
static_cast<wchar_t const*>(s), s.GetLength() |
The static_casts in the final three rows are typically not necessary if the compiler knows that it has to produce a wchar_t const*.
Unfortunately, combining these two tables gives you a horrible n × n matrix of possibilities. Fortunately, you can reduce this to linear complexity by using std::wstring_view as a common currency.
std::wstring_view to_wstring_view(std::wstring const& s)
{
return s;
// or: return { s.c_str(), s.size() };
}
std::wstring_view to_wstring_view(HSTRING s)
{
UINT32 size;
auto p = WindowsGetStringRawBuffer(s, &size);
return { p, size };
}
std::wstring_view to_wstring_view(Platform::String^ const& s)
{
return { s->Data(), s->Length() };
}
std::wstring_view to_wstring_view(winrt::hstring const& s)
{
return { s.c_str(), s.size() };
}
std::wstring_view to_wstring_view(wchar_t*) = delete;
std::wstring_view bstr_to_wstring_view(BSTR s)
{
return { s, SysStringLen(s) };
}
std::wstring_view to_wstring_view(_bstr_t const& s)
{
return { s, s.length() };
}
std::wstring_view to_wstring_view(CComBSTR const& s)
{
return { s, s.Length() };
}
std::wstring_view to_wstring_view(CStringW const& s)
{
return { s, s.GetLength() };
}
For std::, we can take advantage of the pre-existing wstring_view conversion operator.
Note that the BSTR version has to be named bstr_ because BSTR is a type alias for wchar_t*, so just calling it to_ would make it too easy to pass wchar_t*s that aren’t really BSTRs. We delete the to_wstring_view(wchar_t*) overload so that the compiler won’t choose to convert the pointer to a std::wstring and then use the std::wstring overload, which would result in a dangling pointer.¹
The other half is creating the counted string from a wstring_.
template<typename T>
T size_check(std::wstring_view v)
{
if (v.size() > (std::numeric_limits<T>::max)()) {
throw std::bad_alloc();
}
return static_cast<T>(v.size());
}
template<typename T>
T from_wstring_view(std::wstring_view v) = delete;
template<>
std::wstring from_wstring_view<std::wstring>(std::wstring_view v)
{
return std::wstring(v.data(), v.size());
}
template<>
HSTRING from_wstring_view(std::wstring_view v)
{
HSTRING s;
THROW_IF_FAILED(
WindowsCreateString(v.data(),
size_check<UINT32>(v)));
return s;
}
template<>
Platform::String^ from_wstring_view<Platform::String^>(std::wstring_view v)
{
return ref new Platform::String(
v.data(),
size_check<UINT32>(v));
}
template<>
winrt::hstring from_wstring_view<winrt::hstring>(std::wstring_view v)
{
return winrt::hstring(v.c_str(), size_check<uint32_t>(v));
}
template<>
BSTR from_wstring_view<BSTR>(std::wstring_view v)
{
return ::SysAllocStringLen(v.data(), size_check<UINT>(v));
}
template<>
_bstr_t from_wstring_view<_bstr_t>(std::wstring_view v)
{
return _bstr_t(
::SysAllocStringLen(v.data(), size_check<UINT>(v)),
FALSE);
}
template<>
CComBSTR from_wstring_view<CComBSTR>(std::wstring_view v)
{
return CComBSTR(size_check<int>(v), v.data());
}
template<>
CStringW from_wstring_view<CStringW>(std::wstring_view v)
{
return CStringW(v.data(), size_check<int>(v));
}
You can now convert from one type to another through the pair of functions to_ and from_.
// _bstr_t to winrt::hstring auto s = from_wstring_view<winrt::hstring>(to_hstring_view(b));
You could wrap this in an adapter class, with a bunch of custom conversion operators:
struct counted_string_converter
{
std::wstring_view v;
template<typename T>
counted_string_converter(T const& source) :
v(to_wstring_view(source)) {}
counted_string_converter(std::wstring_view source) :
v(source) {}
static counted_string_converter from_bstr(BSTR source)
{
return counted_string_converter(
bstr_to_wstring_view(source));
}
template<typename T>
operator T() const {
return from_wstring_view<T>(v);
}
};
winrt::hstring GetName();
void example()
{
std::wstring s = counted_string_converter(GetName());
}
The danger with the counted_ is that you might try to store it in an auto, which could produce dangling pointers.
winrt::hstring GetName();
void example()
{
// Oops: Reference to destroyed temporary
auto s = counted_string_converter(GetName());
}
All of this is really awkward, and the simplest approach might be to create custom case-by-case conversions. After all, it’s unlikely that any one program is going to need all n × n conversions.
Bonus chatter: The to_wstring_view is handy if you want to insert these counted strings into a C++ stream.
BSTR s = ⟦something⟧ // This stops at the embedded null. std::wcout << s; // This prints the whole string. std::wcout << bstr_to_wstring_view(s);
¹ If you really want to construct a wstring_view from a pointer to a null-terminated C-style string, then just use the wstring_view constructor.
0 comments