0

So I was looking at the stl, and it seems like, for example, in std::transform, arguments that are function objects are just template parameters so that what exactly happens when the passed function object is called depends on what is passed:

template <class UnaryFunc> 
void call(UnaryFunc f, int c)
{ std::cout << f(c)+c << std::endl;}

int a(int i) { return i; }
struct { int operator()(int i){return i;}} b;
int main()
{
  call(a,2); //function call
  call(b,2); //operator() method of b call
  return 0;
}

This is great because it is very generic: it allows you to use stuff like boost::bind() and pass both functors (like b) and functions (like a) to the same function call.

However, this is too lenient. For example, passing a function with the signature int (int i, int j) would cause no errors until the call in call. If call were more complicated, this could lead to confusing errors that are hard to parse. I think this would all be avoided if there were a way to force the caller to only pass functors and functions with a certain signature. Is this possible?

noobermin
  • 203
  • 1
  • 5
  • 5
    "has no type": not possible in C++. "the resulting warning might be vague": your problem statement is vague, it is not clear to me what the problem is. Are you maybe looking for *concepts*? – Marc Glisse May 30 '14 at 21:45
  • It is completely unclear what you're asking, sorry. – iavr May 30 '14 at 22:11
  • If you pass a function object with two arguments into call (), the only error will occur at the call of f. If call was more complex, the error you'd see during compilation might not be helpful as it would be if one used a type that was a functor instead of a template type. – noobermin May 30 '14 at 22:15
  • 3
    This - incomprehensible error message in generic programming - is an actual problem, which C++ Concepts (lite) tries to solve. Virtual functions are not the answer, since they increase coupling and typically decrease efficiency in both space and time. – dyp May 30 '14 at 22:20
  • Forgive me for not wording this well. @dyp essentially answered my question, I'll rephrase it so you can answer it properly when Iget back to a PC – noobermin May 30 '14 at 23:11

2 Answers2

2

First attempt

Short of Concepts, the best method we have currently is to observe the traits of a function and perform static_asserts 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
 }
mpark
  • 7,574
  • 2
  • 16
  • 18
  • The simpler thing to check is whether `f(arg)` is well-formed. It cannot produce as nice an error message ("UnaryFunc is not a function object taking an argument of type Arg") but it has less problems e.g. with overloaded `operator()` and such. Note that you can't take a member function in `call` since you lack the object to call that function on (`obj->*f`). – dyp May 31 '14 at 12:15
  • 1
    @dyp -- I had considered the first approach in an attempt to provide better error messages. However, I believe I managed to get the best of both worlds with my second attempt. It checks the validity of `f(arg)` with C++14's `std::result_of_t` which SFINAE's out rather than a hard error. I also added a test to show that overloaded `operator()` are supported. – mpark Jun 01 '14 at 02:33
  • 1
    This looks really nice and clean. I wonder if the traits are actually necessary here: `template auto call_impl(UnaryFn u, Arg a, void*) -> decltype( u(a) + a, void() );` should also SFINAE-fail. – dyp Jun 01 '14 at 12:46
  • @dyp indeed you're right. updated to reflect the more concise version. – mpark Jun 01 '14 at 16:09
0

Um, I have an idea. (It requires C++11 at least.)

struct FunctorBase
{
    virtual int operator ()(int i) = 0;
};

template <class UnaryFunc> 
void call(UnaryFunc f, int c)
{
    std::cout << f(c)+c << std::endl;
}

struct SomeFunctor final : public FunctorBase
{
    virtual int operator ()(int i) override { ... }
};

int main()
{
    call(SomeFunctor(), 3);
}

SomeFunctor::operator () is virtual, but SomeFunctor is final. So, if compiler knows its type in compile time, Calling operator () can be optimized by static-binding call. (And compiler knows the type when it specialize template <> void call(SomeFunctor, int), of course.)

And call still uses template to receive functor parameter, so it can still receive functors which don't inherit FunctorBase. (e.g. std::bind, ...)

Of course, SomeFunctor has an unnecessary vptr, and it can be unnecessary overhead. However it's just little overhead of sizeof(void *) - it can be ignored unless you require high-level optimizing.

(And There is the fact that types cannot have a zero size in most case, even if it is empty. So I guess it's not overhead.)


Moreover, if you mind it, the overhead can be reduced:

struct FunctorBase
{
#ifndef CHECK_FUNCTOR
    virtual int operator ()(int i) = 0;
#endif
};

template <class UnaryFunc> 
void call(UnaryFunc f, int c)
{
    std::cout << f(c)+c << std::endl;
}

struct SomeFunctor final : public FunctorBase
{
    int operator ()(int i) { ... }
};

int main()
{
    call(SomeFunctor(), 3);
}

As you know, if the function of base class is virtual, the function of derived class becomes virtual, even if it doesn't declared virtual. It's a trick by using it >o<

Community
  • 1
  • 1
ikh
  • 10,119
  • 1
  • 31
  • 70
  • You'd still need a check inside `call` that `UnaryFunc` is derived from `FunctorBase` or something like that. Which increases coupling. – dyp May 31 '14 at 12:12
  • @dyp Um, that's intended - to use such `boost::bind`, ... If someone use that, we can't check whether `UnaryFunc` is appropriate or not. However I think it's ok - just use `FunctorBase` on certain functor, which you want to make sure. – ikh May 31 '14 at 12:41
  • So the only safety you want to add is when I remember to derive from `FunctorBase`, that I define the correct member function? Note that even if you use `std::bind`, you can still very easily wrap that inside a class derived from `FunctorBase`. – dyp May 31 '14 at 12:48
  • @dyp Oh, I don't think wrapping.. However, if I use wrapper, the error message is created from wrapper's code - I think there's little benefit. Even if we don't use any tricks, error message is created from somewhere, anyway. – ikh May 31 '14 at 13:02