C++17 check for valid expression

Viewed 758

Basing my code on this and this, I've written this version:

#include <iostream>
#include <boost/preprocessor.hpp>
#include <boost/callable_traits/is_invocable.hpp>

#define IS_VALID_EXPANDER_BEGIN(count)                    \
    [](BOOST_PP_REPEAT(count, IS_VALID_EXPANDER_MIDDLE, \
        _)) constexpr->decltype IS_VALID_EXPANDER_END

#define IS_VALID_EXPANDER_MIDDLE(z, idx, _) BOOST_PP_COMMA_IF(idx) auto _##idx

#define IS_VALID_EXPANDER_END(...) \
    (__VA_ARGS__){})

#define IS_VALID(...)                              \
    is_valid<__VA_ARGS__>(IS_VALID_EXPANDER_BEGIN( \
        BOOST_PP_VARIADIC_SIZE(__VA_ARGS__))

template <typename... Ts, typename TF>
static constexpr auto is_valid(TF)
{
    return boost::callable_traits::is_invocable<std::decay_t<TF>(Ts...), Ts...>{};
}

struct Test {};

int main()
{
    std::cout << IS_VALID(std::ostream&, double)(_0 << _1) << std::endl;
    std::cout << IS_VALID(std::ostream&, Test)(_0 << _1) << std::endl;
}

However, the results are:

1
1

I can't figure out why.

2 Answers

Your lambda is taking arguments by value, which doesn't allow you to test std::ostream& << T. Change your macro to:

#define IS_VALID_EXPANDER_BEGIN(count)                    \
    [](BOOST_PP_REPEAT(count, IS_VALID_EXPANDER_MIDDLE, \
        &&_)) constexpr->decltype IS_VALID_EXPANDER_END

Also, your use of is_invocable is wrong - it should be:

template <typename... Ts, typename TF>
static constexpr auto is_valid(TF)
{
    return boost::callable_traits::is_invocable<std::decay_t<TF>, Ts...>{};
}

(You should use std::is_invocable anyway, it is available in C++17.)

live example on wandbox.org

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

template<class F>
constexpr auto invokeable( F&& f ) {
  return [](auto&&...args) {
    return std::is_invocable< F&&, decltype(args)... >{};
  };
}

now we can do with far less macro magic:

std::cout << invokeable([](auto& lhs, auto&& v) RETURNS( lhs << v ))( std::cout, 0.0 ) << std::endl;
std::cout << invokeable([](auto& lhs, auto&& v) RETURNS( lhs << v ))( std::cout, Test{} ) << std::endl;

Live example.

If _0 << _1 builds a SFINAE friendly callable object (as it appears to), you could even do:

std::cout << invokeable(_0 << _1)( std::cout, Test{} ) << std::endl;

invokeable is a function object adapter. It turns a function object into a tester to see if arguments passed to it are legal.

It does require you to actually have the arguments of the right type. You can get around this as follows:

template<class T>
struct tag_t { using type=T; };
template<class T>
constexpr tag_t<T> tag{};

template<class X>
struct untag { using type=X; };
template<class X>
using untag_t = typename untag<X>::type;
template<class T>
struct untag<tag_t<T>> { using type=T; };
template<class T>
struct untag<tag_t<T>&&> { using type=T; };
template<class T>
struct untag<tag_t<T>&> { using type=T; };
template<class T>
struct untag<tag_t<T>const &> { using type=T; };

then modify:

template<class F>
constexpr auto invokeable( F&& f ) {
  return [](auto&&...args) {
    return std::is_invocable< F&&, untag_t<decltype(args)>... >{};
  };
}

which permits passing tag<std::ostream&> instead of an lvalue of type std::ostream& to test if all std::ostream& would work in that slot.

Live example.

The only macro used here is RETURNS, which makes making SFINAE-friendly lambdas easier. And there are a number of proposals to add something equivalent to RETURNS in in any case.

Related