First attempt
Short of Concepts
, the best method we have currently is to observe the traits of a function and perform static_assert
s on them. For example,
#include <iostream>
#include <functional>
#include <type_traits>
/**
* Function traits helpers.
**/
template <typename>
struct fail : std::integral_constant<bool, false> {};
template <typename ReturnType, std::size_t Arity>
struct alias {
static constexpr std::size_t arity = Arity;
using return_type = ReturnType;
}; // alias
/**
* A few useful traits about functions.
**/
template <typename T, typename Enable = void>
struct function_traits {
static_assert(fail<T>::value, "not a function.");
}; // function_traits
/* Top-level functions. */
template <typename R, typename... Args>
struct function_traits<R (*)(Args...)> : alias<R, sizeof...(Args)> {};
/* Const member functions. */
template <typename R, typename Class, typename ...Args>
struct function_traits<R (Class::*)(Args...) const> : alias<R, sizeof...(Args)> {};
/* Non-const member functions. */
template <typename R, typename Class, typename ...Args>
struct function_traits<R (Class::*)(Args...)> : alias<R, sizeof...(Args)> {};
/* operator() overloads. */
template <typename T>
struct function_traits<T, std::enable_if_t<std::is_class<T>::value>>
: function_traits<decltype(&T::operator())> {};
/* std::function. */
template <typename R, typename ...Args>
struct function_traits<std::function<R (Args...)>> : alias<R, sizeof...(Args)> {};
/**
* Example contraints on UnaryFunc.
**/
template <typename UnaryFunc, typename Arg>
void call(UnaryFunc f, Arg arg) {
static_assert(function_traits<UnaryFunc>::arity == 1,
"UnaryFunc must take one parameter.");
static_assert(
std::is_integral<typename function_traits<UnaryFunc>::return_type>::value,
"UnaryFunc must return an integral.");
std::cout << f(arg) + arg << std::endl;
}
int a(int i) { return i; }
struct {
int operator()(int i){ return i; }
} b;
int c(int i, int j) { return i + j; }
std::string d(int) { return ""; }
int main() {
call(a, 2); // function call
call(b, 2); // operator() method of b call
// call(1, 2); // static_assert: "not a function".
// call(c, 2); // static_assert: "UnaryFunc must take one parameter".
// call(d, 2); // static_assert: "UnaryFunc must return an integral".
}
Second attempt
This attempt addresses limitations of the first approach, which mainly is the fact that the unary_fn
couldn't be overloaded. It also adds a more restricted test where the result of f(arg)
and arg
can be added.
Note: C++14 version of std::result_of_t
is required here, since the C++11 version doesn't give SFINAE behavior.
#include <iostream>
#include <type_traits>
/**
* Useful utilities
**/
template <typename...>
struct success : std::true_type {};
template <typename...>
struct fail : std::false_type {};
/**
* 'is_addable' type_trait.
* Takes two types and tests if they can be added together.
**/
template <typename Lhs, typename Rhs>
success<decltype(std::declval<Lhs>() + std::declval<Rhs>())>
is_addable_impl(void *);
template <typename Lhs, typename Rhs>
std::false_type is_addable_impl(...);
template <typename Lhs, typename Rhs>
struct is_addable : decltype(is_addable_impl<Lhs, Rhs>(nullptr)) {};
/**
* 'call' implementation.
* If the result of unary_fn(arg) can be added to arg, dispatch to the first
* overload, otherwise provide a static asertion failure.
**/
template <typename UnaryFn, typename Arg>
std::enable_if_t<is_addable<std::result_of_t<UnaryFn (Arg)>, Arg>::value,
void> call_impl(UnaryFn unary_fn, Arg arg, void *) {
std::cout << unary_fn(arg) + arg << std::endl;
}
template <typename UnaryFn, typename Arg>
void call_impl(UnaryFn unary_fn, Arg arg, ...) {
static_assert(fail<UnaryFn, Arg>::value,
"UnaryFn must be a function which takes exactly one argument "
"of type Arg and returns a type that can be added to Arg.");
}
template <typename UnaryFn, typename Arg>
void call(UnaryFn unary_fn, Arg arg) {
return call_impl(unary_fn, arg, nullptr);
}
/**
* Tests.
**/
int a(int i) { return i; }
struct {
int operator()(int i){ return i; }
std::string operator()(std::string s){ return s; }
} b;
int c(int i, int j) { return i + j; }
std::string d(int) { return ""; }
int main() {
call(a, 2); // function call
call(b, 2); // operator() method of b call
call(b, "hello"); // operator() method of b call
// call(1, 2); // static_assert fail
// call(c, 2); // static_assert fail
// call(d, 2); // static_assert fail
}
Third attempt (Thanks @dyp for the suggestion)
EDIT: By adding the operator<<
calls into the decltype
, we get an additional test that that the result of unary_fn(arg) + arg
is also printable.
If is_addable
is a useful type trait, it can be factored out as per the second attempt.
If not, however we can simply perform SFINAE
inline in decltype
. Even shorter and cleaner.
#include <iostream>
#include <type_traits>
template <typename...>
struct fail : std::false_type {};
/**
* 'call' implementation.
* If the result of unary_fn(arg) can be added to arg, dispatch to the
* first overload, otherwise provide a static asertion failure.
**/
template <typename UnaryFn, typename Arg>
auto call_impl(UnaryFn unary_fn, Arg arg, void *)
-> decltype(std::cout << unary_fn(arg) + arg << std::endl, void()) {
std::cout << unary_fn(arg) + arg << std::endl;
}
template <typename UnaryFn, typename Arg>
void call_impl(UnaryFn unary_fn, Arg arg, ...) {
static_assert(fail<UnaryFn, Arg>::value,
"UnaryFn must be a function which takes exactly one argument "
"of type Arg and returns a type that can be added to Arg.");
}
template <typename UnaryFn, typename Arg>
void call(UnaryFn unary_fn, Arg arg) {
call_impl(unary_fn, arg, nullptr);
}
/**
* Tests.
**/
int a(int i) { return i; }
struct {
int operator()(int i){ return i; }
std::string operator()(std::string s){ return s; }
} b;
int c(int i, int j) { return i + j; }
std::string d(int) { return ""; }
int main() {
call(a, 2); // function call
call(b, 2); // operator() method of b call
call(b, "hello"); // operator() method of b call
// call(1, 2); // static_assert fail
// call(c, 2); // static_assert fail
// call(d, 2); // static_assert fail
}