An unexpected brush with “strong typedefs”
Today I failed to refactor a piece of code. Simplified:
int extract_port(const std::string& hostport)
{
auto pos = hostport.find(':');
if (pos != std::string::npos) {
return std::stoi(hostport.substr(pos+1));
}
return -1;
}
The original code didn’t use std::stoi
, mind you.
It used a different, third-party library API that expected
to take const std::string&
.
I tried to apply the typical C++17 const string& -> string_view
refactoring.
int extract_port(std::string_view hostport)
{
auto pos = hostport.find(':');
if (pos != std::string::npos) {
return std::stoi(hostport.substr(pos+1));
}
return -1;
}
And it failed to compile.
Why? Doesn’t std::string_view
provide all the same API as std::string
?
The compiler is perfectly happy with the references to hostport.find(':')
and hostport.substr(pos+1)
. What could be going wrong?
One of the incidental inefficiencies in the original code is that
std::string::substr
returns a string.
Since substr
has str
right there in the name, we shouldn’t be too surprised
that it returns a string
. We may wish std::string
had a subview
method
(substr_view
? slice
?), but it doesn’t.
Our refactoring quietly changed the code from calling string::find
to
calling string_view::find
, and from calling string::substr
to
calling string_view::substr
. The former is a drop-in replacement: both
versions of find
return size_t
. But the latter is surprisingly not
a drop-in replacement! When the Committee standardized string_view
, they
did a drive-by fix for this inefficiency: std::string_view::substr
returns
string_view
, not string
.
Incidentally, this means that the way to spell our hypothetical
mystring.subview(pos, len)
in standard C++17 isstd::string_view(mystring).substr(pos, len)
. Not too bad, as long as you trust your readers to know the trick.
So, the type of hostport.substr(pos+1)
has changed from string
to string_view
.
And std::stoi
doesn’t accept string_view
. (Why?
Because it’s a thin wrapper around strtol
, and strtol
requires null-termination.
It’s a sordid story. Anyway, remember, my actual code used a different API there.
This was not the fault of std::stoi
in particular.)
It occurred to me that this story relates directly to the perennial impossibility of “strong typedefs.” What we had in the STL, pre-C++17, was a single type shaped roughly like this:
struct S {
size_t find(char);
S substr(size_t);
};
And then in C++17, we did the manual equivalent of
strong_typedef SV = S;
Which produced a brand-new struct type SV
shaped roughly like this:
struct SV {
size_t find(char);
SV substr(size_t);
};
But is that what the user wanted? Or did the user want
struct SV {
size_t find(char);
S substr(size_t);
};
instead? In this particular case, it seems that some users wanted the one thing, and some other users wanted the other. It really depends on how the type is going to be used.
In this case, both options were defensible. But,
given that string_view
’s raison d’être was to be a mechanical
drop-in replacement for parameters of type const std::string&
,
I think we might have been better off with a
string_view::substr
that returned string
.
Furthermore, I think that C++2a should be delayed. (Previously on this blog: 1, 2, 3)