I have a library with several function objects that might only accept a few types depending on std::is_integral
. I want std::is_invocable
to return false
when the condition fails, but I also want a nice static_assert
error message when a user tries to call an instance of the function object. Here is a simplified example of the function objects I currently have:
struct function
{
template<typename Iterator>
auto operator()(Iterator first, Iterator last) const
-> std::enable_if_t<std::is_integral_v<
typename std::iterator_traits<Iterator>::value_type
>>
{ /* something */ }
};
With such an implementation, std::is_invocable
is std::false_type
as expected when the SFINAE condition is not met, but users encounter ugly SFINAE error messages when they try to call the function object with parameters that don't meet the SFINAE condition.
To get better error messages, I tried the following solution instead:
struct function
{
template<typename Iterator>
auto operator()(Iterator first, Iterator last) const
-> void
{
static_assert(std::is_integral_v<typename std::iterator_traits<Iterator>::value_type>,
"function can only be called with a collection of integers");
/* something */
}
};
With this implementation, users get friendly error messages when the original SFINAE condition is not met, but std::is_invocable
is std::true_type
when asked whether a function
instance can handle a type that does not satisfy std::is_integral
.
I tried several tricks and variations involving decltype(auto)
, if constexpr
and other mechanisms, but couldn't get a class where error messages were nice and where std::is_invocable
corresponded to the expected std::false_type
when asking whether function
could be invoked with incorrect types.
What am I missing here? Is there a way to get both proper std::is_invocable
and user-friendly error messages?
Here's one awful way. Add an overload:
template <typename Iterator>
auto operator()(Iterator first, Iterator last) const
-> std::enable_if_t<std::is_integral_v<
typename std::iterator_traits<Iterator>::value_type
>>;
template <typename Iterator, class... Args>
void operator()(Iterator, Iterator, Args&&... ) const = delete; // must be integral
This satisfies the is_invocable<>
condition, since the contrained function template is more specialized and preferred - so if the condition is met, the function is invocable, else it's deleted.
This does a little bit better on the error case, since if you try to call it, you get:
foo.cxx: In function ‘int main()’:
foo.cxx:31:13: error: use of deleted function ‘void function::operator()(Iterator, Iterator, Args&& ...) const [with Iterator = std::__cxx11::basic_string<char>*; Args = {}]’
f(&i, &i);
^
foo.cxx:19:10: note: declared here
void operator()(Iterator, Iterator, Args&&... ) const = delete; // must be integral
^~~~~~~~
The comment shows up in the error message, which is sort of like a static assert message?
This is actually one of the motivations for Concepts. With a requires
instead of an enable_if
, we get:
foo.cxx: In function ‘int main()’:
foo.cxx:26:13: error: no match for call to ‘(function) (std::__cxx11::string*, std::__cxx11::string*)’
f(&i, &i);
^
foo.cxx:11:10: note: candidate: void function::operator()(Iterator, Iterator) const requires is_integral_v<typename std::iterator_traits<_Iter>::value_type> [with Iterator = std::__cxx11::basic_string<char>*]
void operator()(Iterator first, Iterator last) const
^~~~~~~~
foo.cxx:11:10: note: constraints not satisfied
foo.cxx:11:10: note: ‘is_integral_v<typename std::iterator_traits<_Iter>::value_type>’ evaluated to false
That's... a little better I guess.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With