3

I'm trying to write a generic function that concatenates two functions that can be called with the same set of arguments but I'm having a bit of trouble. Here's what I have so far (it doesn't compile)

//A functor to store the input functions and call them
template <typename LEFT, typename RIGHT>
struct combine_functions {
  combine_functions(const LEFT &left, const RIGHT &right)
   : left(left), right(right) {}

  template <typename ...ARGS>
  std::enable_if_t<
    //My compiler doesn't have support for C++17 std library so I 
    //found an implementation of callable on SO
    is_callable_v<LEFT, std::decay_t<ARGS>...> &&
    is_callable_v<RIGHT, std::decay_t<ARGS>...>
  > operator()(ARGS... args) const {
    //the return value doesn't matter in my situation and can be 
    //completely discarded
    left(std::forward<ARGS>(args)...);
    right(std::forward<ARGS>(args)...);
  }

private:
  mutable LEFT left;
  mutable RIGHT right;
};

//I should probably have an enable if that checks the arguments 
//are function pointers or functors
template <typename LEFT, typename RIGHT>
combine_functions<
  std::decay_t<LEFT>,
  std::decay_t<RIGHT>
>
operator+(
  const LEFT &left,
  const RIGHT &right
) {
  return {left, right};
}

If it is unclear what I'm trying to achieve then here is a test.

#include <iostream>
#include "combine functions.hpp"    

struct A {
  void operator()(float &f, int i) {
    std::cout << "running A with float " << f << " and int " << i << '\n';
    f++;
  }
};

struct B {
  void operator()(float &f, int i) {
    std::cout << "running B with float " << f << " and int " << i << '\n';
    f++;
  }
};

struct C {
  void operator()(float &f, int i) {
    std::cout << "running C with float " << f << " and int " << i << '\n';
    f++;
  }
};

int main(int, const char**) {
  A a;
  B b;
  C c;
  auto abc = concat(concat(a, b), c);
  //or
  //auto abc = a + b + c;
  std::function<void(float &, int)> abcFunc = abc;
  float f = 5.0f;
  int i = 9;
  abcFunc(f, i);

  return EXIT_SUCCESS;
}

And here is the expected output

running A with float 5 and int 9
running B with float 6 and int 9
running C with float 7 and int 9    
  • How do I implement this in C++?
  • Is it unwise to use an overloaded operator in this situation?
  • Is "concatenate" the best term for this operation?
halfer
  • 19,824
  • 17
  • 99
  • 186
Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50
  • 1
    The `enable_if_t` is basically unnecessary. Post what fails to compile, with the errors – Caleth May 15 '17 at 08:39
  • What compiler are you using, and what errors do you get? A fishy part (that doesn't affect anything at compile time) is to use `std::forward` twice on the same argument - if the first function consumes an rvalue, there is nothing left for the second function. – Bo Persson May 15 '17 at 08:40
  • Please provide an [mcve] including any third party code you have used. An incomplete fragment is meaningless. An error could be anywhere. – n. m. could be an AI May 15 '17 at 08:46
  • @n.m. Normally yes. I would argue this is acceptable as the OP has no idea _how_ to go about this code. It's minimal and well documented in terms of desired result and they have demonstrated due diligence in attempting to solve the problem. – Qix - MONICA WAS MISTREATED May 15 '17 at 08:50
  • Your operator+ will apply to objects of all types, even non-callables. This is probably where you want your enable_if. But it's generally bad practice to create overloaded operators for any classes you didn't create, and you didn't write the std::function class or function pointer "class", so I'd scrap that altogether. Besides, I'd expect `(f+g)()` to be the same as `f()+g()`; see [function spaces on Wikipedia](https://en.wikipedia.org/wiki/Vector_space#Function_spaces) – Arthur Tacca May 15 '17 at 08:56

4 Answers4

3

I think this is a reasonable starting point. Supports any number of concatenations and any number of arguments with perfect forwarding:

#include <tuple>
#include <utility>
#include <iostream>

namespace detail 
{
    template<class Tuple, std::size_t...Is, class...Args>
    void exec(Tuple&& tuple, std::index_sequence<Is...>, Args&&...args)
    {
        using expand = int[];
        void(expand{
            0,
            (std::get<Is>(tuple)(std::forward<Args>(args)...),0)...
        });

    }
}

template<class...Funcs>
auto concat(Funcs&&...funcs)
{
    constexpr auto nof_funcs = sizeof...(funcs);
    return [funcs = std::make_tuple(std::forward<Funcs>(funcs)...)](auto&&...args) mutable
    {
        detail::exec(funcs, 
                     std::make_index_sequence<nof_funcs>(), 
                     std::forward<decltype(args)>(args)...);
    };
};

int main()
{
    auto f1 = [](auto&& arg) { std::cout << arg << std::endl; };
    auto f2 = [](auto&& arg) { std::cerr << arg << std::endl; };

    concat(f1, f2)("Hello, World");
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • I plugged this into my code base and it works perfectly but I don't really understand the code. I feel guilty using someone else's code if don't really understand it. Could you describe in detail what it does and how it does it? Also, in the place where I think you're calling the functions, you're using `forward`. But Jarod42 said that doing so will move the object multiple times. – Indiana Kernick May 15 '17 at 09:35
  • 2
    @Kerndog73 To understand what it does, look the documentation for [`std::apply`](http://en.cppreference.com/w/cpp/utility/apply). As for the forwarding, here no pack is forwarded twice except in `expand`. `args` shouldn't be forwarded there – Rerito May 15 '17 at 09:51
  • @Rerito I can think of situations where forwarding the args is correct, and situations where it is not. Perhaps a better solution is to write some code that detects references and then passes a std::ref/cref as appropriate. Hence "starting point". The fully generic solution would be non-trivial. – Richard Hodges May 15 '17 at 09:59
2

You might use the following:

template <typename LEFT, typename RIGHT>
struct combine_functions {
private:
  LEFT left;
  RIGHT right;
public:
  combine_functions(const LEFT& left, const RIGHT& right)
   : left(left), right(right) {}

  template <typename ...ARGS>
  auto operator()(ARGS&... args) const
  -> decltype(left(args...), static_cast<void>(right(args...)))
  {
    left(args...);
    right(args...);
  }

};

template <typename LEFT, typename RIGHT>
combine_functions<std::decay_t<LEFT>, std::decay_t<RIGHT>>
concat(const LEFT& left, const RIGHT& right)
{
  return {left, right};
}

Demo

I don't use operator + which is too generic and match too many type. I don't use std::forward as you don't want to move(right would called moved objects...)

Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

How do I implement this in C++?

I'm not usually one to just write code for someone but this was simple enough.

#include <iostream>
#include <functional>
#include <string>

using namespace std;

template <typename Left, typename Right>
class ConcatFn {
public:
    ConcatFn(Left left, Right right)
            : left(left)
            , right(right) {
    }

    template <typename... Args>
    void operator()(Args... args) {
        this->left(args...);
        this->right(args...);
    }

private:
    function<Left> left;
    function<Right> right;
};

void A(const char *foo) {
    cout << "A: " << foo << endl;
}

void B(string bar) {
    cout << "B: " << bar << endl;
}

int main() {
    ConcatFn<void(const char *), void(string)> fn(A, B);

    fn("hello!");

    return 0;
}

Outputs:

$ ./concat
A: hello!
B: hello!

I don't think you're going to get away from the template arguments on the fn declaration above.

Also, if you want to guarantee that the functions have the exact signature, just remove the second template argument (Right) and use Left (ideally rename it) everywhere Right is used.

Is it unwise to use an overloaded operator in this situation?

Not at all.

Is "concatenate" the best term for this operation?

Probably not, but seeing as how this use-case is kind of rare I'm not aware of any 'standard' term for this. Function chaining or grouping, maybe?

Qix - MONICA WAS MISTREATED
  • 14,451
  • 16
  • 82
  • 145
0

Looks about right, just a few quirks here and there

#include<type_traits>
#include<utility>

template<typename L, typename R>
struct combined
{
    typename std::decay<L>::type l;
    typename std::decay<R>::type r;
    combined(L&& l, R&& r)
       : l(std::forward<L>(l)), r(std::forward<R>(r)) {}

    template<typename... Args>
    void operator()(Args&&... args)
    {
        l(args...);
        r(std::forward<Args>(args)...);
    }
};

template<typename L, typename R>
combined<L, R> combine(L&& l, R&& r)
{
    return {std::forward<L>(l), std::forward<R>(r)};
}

First, the callable objects most likely have to be stored, hence the std::remove_reference.

std::enable_if ais needless since compiler will emit error when object is called incorrectly.

std::forward is known as perfect forwarding, and is important.

EDIT3 After debate, the first forward was taken off to prevent unintentional moves happening, thanks to Rerito, R. Martinho Fernandes and Jarod42

operator+ feels really weird since you are not adding the returned values, this conflicts with the definition of function addition.

Community
  • 1
  • 1
Passer By
  • 19,325
  • 6
  • 49
  • 96
  • 2
    You can't use `args...` once you `std::forward`-ed them because some of the arguments carried by the packs might have been moved... Remove the first call to `std::forward`: `l(args...);`. And your constructor should be templated if you want to use forward on its arguments. – Rerito May 15 '17 at 09:38
  • I haven't thought about moving causing problems at first, I just thought this should be perfect forwarded. After some thought, I decided that I actually do mean that, this should just be some syntax sugar around calling two functions, if the user didn't want to move, just don't give a function that moves. – Passer By May 15 '17 at 13:43
  • @Rerito And why do I need to template the constructor? – Passer By May 15 '17 at 13:57
  • Because `forward` is meant to be used along with template argument deduction. It doesn't make sense otherwise. Assuming `L` is say `std::function`, then your constructor only accepts `std::function&&`. This can only bind rvalues... Not quite the goal of perfect forwarding isn't it? – Rerito May 15 '17 at 14:12
  • I thought `L` would deduce to the proper types via the helper function? I never did intend the template class to be used directly, it wouldn't be much use on lambdas anyways – Passer By May 15 '17 at 14:13
  • On second thoughts, this might be a bad idea under C++17 class template deduction – Passer By May 15 '17 at 14:16
  • [Demo](https://ideone.com/ccdev6) of the problematic part of 2 forward. – Jarod42 May 15 '17 at 14:19
  • 1
    My bad, I didn't notice the helper. However, I still stand by my word, a double `forward` is potentially harmful. `forward`ing is used along with template argument deduction (and thus all depends on the value categories of the arguments passed at each call site). If the user calls with a prvalue, then he potentially will have a bad time with that implementation – Rerito May 15 '17 at 14:19
  • @Rerito Not if the function never moves from them, no. The user is free to use this exactly as a wrapper around two function calls in this implementation, and I do think that is better. – Passer By May 15 '17 at 14:20
  • @Jarod42 I have rejected what you demonstrated there as a *choice* of the user. If you wrote the two functions by hand without the `concat` the results would be the same as forwarding it, which is **by design**. That the user cannot choose to do this is by far, much weirder. If you don't want that `string` to get eaten, why are you moving it? – Passer By May 15 '17 at 14:22
  • These moves can be silently hidden if the function takes the corresponding argument by value for instance. Basically you're forbidding the moves. This is equivalent to not forwarding at all. And by not forwarding at all, at least you would have a correct behavior regardless of the value categories of the passed arguments... – Rerito May 15 '17 at 14:27
  • @Rerito No, not really. The user has to choose the left function as not going to destroy the arguments and then move with the right function. If your left function only takes lvalue references, nothing will happen with them. Again, this is literally the same as writing out both functions by hand. – Passer By May 15 '17 at 14:29
  • So you're forbidding the moves from the first function, which is equivalent to not forwarding on the first call and then forwarding on the second. – Rerito May 15 '17 at 14:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/144265/discussion-between-passer-by-and-rerito). – Passer By May 15 '17 at 14:31