Last Update:
How To Detect Function Overloads in C++17/20, std::from_chars Example
Table of Contents
The problem: a library function offers several overloads, but depending on the implementation/compiler, some of the overloads are not available. How to check the existence of an overload? And how to provide a safe fallback?
In this article, I’ll show you a background “theory” and one case - std::from_chars
that exposes full support for numbers or only integer support (in GCC, Clang).
Update 2021: We’ll also throw some C++20 concepts :)
Intro - Function Overloads
Before we jump into a more complex problem, let’s start with something simpler. This will allow us to understand the final solution easily.
Imagine a library that provides a function Compute()
:
// lib V1:
void Compute(int in, int& out) { }
Later in the second version of the library, you’ll have a new overload.
// lib V2:
void Compute(int in, int& out) { }
void Compute(double in, double& out) { }
The problem is that you want to have support both int
and double
in your project no matter what’s the version of the library used. In a case, the library version doesn’t contain a necessary overload you can provide a custom alternative.
But how to check it effectively?
Using The Preprocessor
If you know the version of the library and you have all required defines, you can use preprocessor and create a following solution:
// provide custom overload for double if we use V1
#if LIB_VERSION == LIBV1
void Compute(double in, double& out) { /* custom code */ }
#endif
In the above code, you use defines and macros to provide a custom overload for the Compute()
function.
This might work, but what if you have another version of the library? With even more complex overloads. The #if
approach might quickly become a mess of preprocessor code. What if we could “detect” if a function has a given overload?
Templates to the Rescue - The Detection Pattern!
What we need is a way to ask the compiler:
// pseudocode:
if (overload Compute(double, double&) not exists) { }
While it’s not possible with macros and preprocessor, you can detect a function existence using templates.
The detection idiom might work in the following way for our Compute()
function:
template <typename T, typename = void>
struct is_compute_available : std::false_type {};
template <typename T>
struct is_compute_available<T,
std::void_t<decltype(Compute(std::declval<T>(),
std::declval<T&>())) >> : std::true_type {};
The above code creates a template structure is_compute_available
. By default, the structure derives from false_type
. But when you provide a T
for which Compute()
has an overload, then we “activate” the partial template specialisation that derives from true_type
.
The core part is void_t
magic that tries to check if the overload is available. If the whole expression is not valid, it’s SFINAEd, and the specialisation is gone. Otherwise, the template specialisation is, and the compiler will select it.
How does std::void_t
work?
std::void_t
is a relatively simple template that can help with SFINAE magic. It was added in C++17 and it’s implementation is surprisingly straightforward:
template< class... >
using void_t = void;
See more info at cppreference
The basic idea is that you can put many compile-time checks, and if something fails, then the whole expression is SFINAEd. This helper type is often used for detection pattern.
For our Compute()
check we use the following code:
template <typename T>
struct is_compute_available<T,
std::void_t<decltype(Compute(std::declval<T>(),
std::declval<T&>())) >> : std::true_type {};
The internal check uses:
decltype(Compute(std::declval<T>(), std::declval<T&>()))
What we do here is we’re trying to find the return type of a function overload that takes std::declval<T>()
and std::declval<T&>()
. std::declval
is a helper (added in C++11) that allows us to “pretend” that we have an object of some type (even if default constructor s not available).
If Compute()
cannot be called with T
and T&
objects, then the compiler will SFINAE the whole expression inside void_t
.
Wrapper Code
Equipped with the tool we can now create the following wrapper code:
// helper variable template
template< class T> inline constexpr bool is_compute_available_v =
is_compute_available<T>::value;
template <typename T>
void ComputeTest(T val)
{
if constexpr (is_compute_available_v<T>)
{
T out { };
Compute(val, out);
}
else
{
std::cout << "fallback...\n";
}
}
You can play with code @Coliru
C++20 Concepts
If you can use a C++20 compiler, then we can make our code much shorter!
Thanks to C++20 Concepts there’s no need to use complicated SFINAE syntax.
Our previous example can be specified with the following concept and requires
expression:
template<typename T>
concept is_compute_available2 = requires(T v, T& out) {
Compute(v, out);
};
All we do is to write almost “natural” code that is check at compile time if can be valid.
We can also do it in one line:
template <typename T>
void ComputeTest(T val)
{
if constexpr (requires(T v, T& out) { Compute(v, out);})
{
T out { };
Compute(val, out);
}
else
{
std:: cout << "fallback...\n";
}
}
Play with code @Compiler Explorer
See more in my blog post on Concepts: C++20 Concepts - a Quick Introduction - C++ Stories
Example - std::from_chars
Ok, so we covered a basic scenario with Compute()
function, but let’s check some more practical example.
How about implementing a fallback for std::from_chars
? This is a robust set of functions that allows fast string to number conversions. I wrote about that feature in my separate article: How to Use The Newest C++ String Conversion Routines.
The problem is that on some compilers (GCC and Clang), as of June 2021 not all conversions are possible. For example, since MSVC 2019 16.4 and GCC 11 you can convert into integral types and also into floating point types, but Clang offers only integer support.
our task is to implement the following helper function:
template <typename T>
[[nodiscard]] std::optional<T> TryConvert(std::string_view sv);
The function takes a string view and then returns optional<T>
. The value will be there if the conversion is possible.
ifdefs
In the code samples for my book, I had explicit #ifdefs
to check if the code is compiled on MSVC and if not, then I provided some fallback function. But then, after discussion with Jacek Galowicz (Technical Reviewer) we tried to use templated based approach.
For example, the basic approach is to check the compiler:
// for GCC/Clang:
#ifndef _MSC_VER
template<>
[[nodiscard]] std::optional<double> TryConvert(std::string_view sv) {
// implementation...
}
#endif
This works, but when GCC and Clang improve the Standard Library implementations, then I have to adjust the code.
Feature test macros
For new C++ features, we can also check their availability by using feature test macros. They are defined for C++20, but most of the compilers support it already.
For from_chars
we have __cpp_lib_to_chars
.
Still, this feature test is too broad as it won’t tell us about the floating point support. It would be nice to have some distinct “sub” features enabled in this case.
See more test macros @cppreference
C++17 Templates - the solution
Let’s try with templates.
Here’s the detection code:
template <typename T, typename = void>
struct is_from_chars_convertible : false_type {};
template <typename T>
struct is_from_chars_convertible<T,
void_t<decltype(from_chars(declval<const char*>(), declval<const char*>(), declval<T&>()))>>
: true_type {};
// std:: omited...
And the function:
template <typename T>
[[nodiscard]] std::optional<T> TryConvert(std::string_view sv) noexcept {
T value{ };
if constexpr (is_from_chars_convertible<T>::value) {
const auto last = sv.data() + sv.size();
const auto res = std::from_chars(sv.data(), last, value);
if (res.ec == std::errc{} && res.ptr == last)
return value;
}
else {
try {
std::string str{ sv };
size_t read = 0;
if constexpr (std::is_same_v<T, double>)
value = std::stod(str, &read);
else if constexpr (std::is_same_v<T, float>)
value = std::stof(str, &read);
if (str.size() == read)
return value;
}
catch (...) { }
}
return std::nullopt;
}
As the fallback code, we’re using stod
or stof
depending on the floating point type. The functions require null-terminated strings, so we have to convert from string view into a string before we pass the parameter. This is not the best approach but might work as a fallback solution.
You can play with the code @Coliru
Add code like std::cout << "fallback...";
to check if a fallback was selected or the proper from_chars
overload.
The code is still not perfect, so I’m happy to see suggestions in the comments. Maybe you can came up with something easier?
C++20 Concepts Solution
With Concepts it’s much easier!
See the code:
template <typename T>
concept is_from_chars_convertible =
requires (const char* first, const char* last, T& out) {
std::from_chars(first, last, out);
};
As you can see, we have a simple syntax and almost natural code.
Play with the updated example here @Compiler Explorer
Switch between GCC 11 and GCC 10, or into Clang - and see what code path is instantiated.
Summary
Working with real examples is better in most of the cases, so I like that we could show how the detection pattern works on a real function: std::from_chars
. The full check used various of techniques: SFINAE, void_t
, decltype
, std::declval
, std::true_type
, std::false_type
and partial template specialisation. Plus we even used if constexpr
!
Additionally, since it’s 2021, we can leverage the power of C++20 Concepts! The code is super simple and very natural to read and write now.
I wonder about the compilation time for such templated code. While the preprocessor approach is old-style and not scalable, it’s super simple, and I guess it offers the best compilation time. Having a single SFINAE detector on a function it usually ok, but what if you have tens or hundreds of such checks? I leave that as an open question.
Do you use detector pattern in your projects? Let us know in comments below!
Here are some good references:
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: