Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting the best of static_assert and std::is_invocable

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?

like image 918
Morwenn Avatar asked Jul 07 '17 20:07

Morwenn


1 Answers

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.

like image 101
Barry Avatar answered Oct 05 '22 22:10

Barry