Resolving overload sets in C++ that differ in accepting
boolvsstd::stringas parameters can cause surprises when we call them with string literals as arguments. We’ll see how we can work around this with concepts.
Consider this overload set, where the first overload accepts an
std::string and the second accepts a bool:
void show(std::string_view s) {
std::cout << "Showing the string: " << s << std::endl;
}
void show(bool b) {
std::cout << "Showing the bool: " << b << std::endl;
}
int main() {
show(true);
show("A lovely string");
}
Both calls select the bool overload and therefore the snippet prints
Showing the bool: 1 twice. This might be surprising, as one could have
expected show("A lovely string") resolving to the std::string_view
overload instead, and such surprises often lead bugs.
Essentially, this happens due to a conversion
sequence
from const char[16] (the underlying type of "A lovely string") to
bool, and that’s preferred over the alternative conversion to
std::string_view.
The same holds if we replace
std::string_viewwithconst std::string&.
There are multiple ways to “fix” this, and each one depends on what we want to achieve, for example:
- not overloading in the first place
- adding an overload that accepts a
const char* - explicitly converting the literal into an
std::string_viewat call-site - use a template with SFINAE
I’d like to present another option that improves upon “use a template with SFINAE” with C++20 concepts.
Concepts for an exact match
With a little help from the standard std::same_as concept (and the
constrained
auto
syntax for terseness), we can restrict the overload that accepts
something implicitly convertible to a bool to accept exactly a
bool. This disables the unwanted conversion to bool in favour of the
conversion to std::string_view:
void show(std::string_view s) {
std::cout << "Showing the string: " << s << std::endl;
}
void show(std::same_as<bool> auto b) {
std::cout << "Showing the bool: " << b << std::endl;
}
int main() {
show(true);
show("A lovely string");
}
Note that bool b has changed to std::same_as<bool> auto b – maybe a
bit more verbose, but it works “as expected”.
To be even more strict, we could also change std::string_view to
std::same_as<std::string_view> auto and thus require an explicit
conversion at call-site:
void show(std::same_as<std::string_view> auto s) {
std::cout << "Showing the string: " << s << std::endl;
}
void show(std::same_as<bool> auto b) {
std::cout << "Showing the bool: " << b << std::endl;
}
int main() {
show(true);
show("A lovely string"sv);
}
Note that we had to explicitly convert the literal into an
std::string_view – in this case using the sv operator, but
std::string_view{"A lovely string"} would work just as fine.
Any attempt to call show("A lovely string") now results in a compile
error.
And that’s possibly safer.