Is there a way to access arguments stored in the function object returned by std::bind() in C++?

150 Views Asked by At

I need a way to decompose the function object returned by std::bind() to its arguments in a function template.

The code snippet below shows what I'd like to do:

#include <iostream>
#include <functional>

void foo(int x, double y, char z)
{
    std::cout << "x = " << x << ", y = " << y << ", z = " << z << '\n';
}

int main()
{
    auto f = std::bind(foo, 42, 3.14, 'a');
    std::cout << "The first argument is: " << std::get<0>(f) << '\n';
    std::cout << "The second argument is: " << std::get<1>(f) << '\n';
    std::cout << "The third argument is: " << std::get<2>(f) << '\n';
}

The output should be:

The first argument is: 42
The second argument is: 3.14
The third argument is: a

But it doesn't compile with the error:

test.cpp:12:60: error: no matching function for call to ‘get<0>(std::_Bind<void (*(int, double, char))(int, double, char)>&)’

Why do I need this?

std::generate() and std::generate_n() can accept both an engine or a distribution to generate random numbers. When you pass a distribution, a standard way is std::bind in addition to functors and lambdas:

std::mt19937 R(666);
std::::uniform_int_distribution<int> D(0, 100);
std::vector<int> v(10);
std::generate(v.begin(), v.end(), std::bind(D, std::ref(R));

I've developed a parallel generate() algorithm that plays fair (i.e. you always get the same sequence if you use the same seed and distribution). I'd like it to have the same interface as std::generate() but the algorithm requires a call to the discard() function of the engine, which is unnecessary for the serial algorithm. My idea is to use std::is_bind_expression<T>::value to see if an engine or a distribution is passed. If it's an engine, call discard() and if it's a bind expression, first decompose it and then call discard() on the engine. Lambdas are not going to work here because the algorithm can't access the engine. If it's not possible with the std::bind implementation, then I'll implement my version of std::bind.

If there's a better way to do this, I'll be more than glad to know it.

2

There are 2 best solutions below

0
YSC On BEST ANSWER

This is not possible.

std::bind returns an unspecified type with only the following guaranteed:

  • a copy or move constructor;
  • (deprecated) result_type type alias;
  • operator().

If you need the feature described in your question, you must come up with your own definition of bind.

7
463035818_is_not_an_ai On

std::bind can be considered deprecated and is replaced by lambdas. I heard about cases where std::bind is still needed, but I never met them.

You can write your custom function wrapper that stores the arguments in a tuple.

#include <functional>
#include <tuple>
#include <iostream>

template <typename R,typename BoundTuple,typename ... Args>
struct my_function {
    BoundTuple bound;
    std::function<R(Args...)> func;
    template <typename...MoreArgs>
    R operator()(MoreArgs&& ... more) {
        return std::apply(func,std::tuple_cat(bound,std::tuple(more...)));
    }
};

template <typename R,typename ... Bound,typename ... Args>
my_function<R,std::tuple<Bound...>,Args...> my_bind(std::function<R(Args...)>&& f,Bound&&...b){
    return {{b...},f};
}

void foo(int x, double y, char z) {
    std::cout << "x = " << x << ", y = " << y << ", z = " << z << '\n';
}

int main()
{
    auto f = my_bind(std::function(foo), 42, 3.14, 'a');
    std::cout << "The first argument is: " << std::get<0>(f.bound) << '\n';
    std::cout << "The second argument is: " << std::get<1>(f.bound) << '\n';
    std::cout << "The third argument is: " << std::get<2>(f.bound) << '\n';
    f();
}

Live Demo

However, take this with a grain of salt. Refernces are not handled and only arguments starting from left can be bound. Its just to illustrate the approach. Whether this approach can be extended to fir your needs depends on details.

In my humble opinion the interface of std::bind is terrible (e.g "If some of the arguments that are supplied in the call to g() are not matched by any placeholders stored in g, the unused arguments are evaluated and discarded." yikes!), which can be taken as evidence that it is not trivial to get it right.