Reputation: 22378
I was using a std::uniform_int_distribution
to generate primes (p)
. I put the distribution object in an anonymous namespace - which seems like C++ 'static linkage' for grown-ups...
namespace
{
// a more pedantic range: [2, 18446744073709551557]
std::uniform_int_distribution<uint64_t> p_dist {2};
std::mt19937 rng; // (sufficient IV states for uniqueness)
}
Note, that I seed the Mersenne Twister as thoroughly as portable code will allow. This isn't really important to the question though. It's just to assure the reader I'm using the random facilities properly:
std::seed_seq::result_type data[rng.state_size];
std::random_device rdev;
std::generate_n(data, rng.state_size, std::ref(rdev));
std::seed_seq rng_seed (data, data + rng.state_size);
rng.seed(rng_seed);
This is very convenient, as I have a deterministic u64_prime(p)
function, using (7) bases
, that can determine if (p)
is prime:
uint64_t p;
while (!u64_prime(p = p_dist(rng)))
;
Now I create a std::function
object:
std::function<uint64_t()> zp_rng = std::bind(
decltype(p_dist){0, p - 1}, std::ref(rng));
This function: zp_rng()
can be invoked to return a random number in Z(p)
. That is, using the distribution object for: [0, p - 1]
from the results of the referenced rng.
Now this is very impressive - but I've effectively adopted it by cut-and-paste with little understanding of the interaction between std::function
and the interaction of the parameters given to std::bind
.
I'm not confused by decltype(p_dist){0, p - 1}
- that's just a way to specify we still want to use a std::uniform_int_distribution
. My understanding of std::ref(rng)
is that it prevents a local copy of the rng being instantiated, and forces the use of reference instead... so:
Q: What are the basic rules that effectively determine: dist(rng)
being used - I don't see why std::bind
would enforce this interaction. A lot of interactions seem based around operator ()
methods.
Q: std::function
is helpfully referred to as 'a general-purpose polymorphic function wrapper' on cppreference.com. So is it a function that encapsulates a uint64_t
return type? Or again, making use of operator ()
syntax to drive the notion of a function?
As incredibly useful as these constructs are, I feel like I'm cargo-cult programming to a degree here. I'm looking for an answer which resolves any ambiguities in a concrete way, and adds insight to similar questions - how are bind
arguments expected to interact, and how does the function
signature reflect that?
I'm not getting any positive feedback about the use of std::bind
. Plenty on the superior results (and code generation) of simply using lambda functions, even in such a simple case. My own tests validate this.
Upvotes: 3
Views: 1459
Reputation: 171433
Q: What are the basic rules that effectively determine: dist(rng) being used - I don't see why std::bind would enforce this interaction. A lot of interactions seem based around operator () methods.
std::bind
performs function composition. The first argument must be a function object, i.e. something callable like a function (e.g. a normal function, or a class with an overloaded operator()
).
A call to std::bind
makes copies of its arguments, "binds" the copies of the arguments to the first argument (the function object), and returns a new function object that will invoke the copy of the function object.
So in a simple case:
int f(int i) { return i; }
auto f1 = std::bind(f, 1);
this binds the value 1
to the function f
, creating a new function object that can be called with no arguments. When you invoke f1()
it will invoke f
with the argument 1
, i.e. it will call f(1)
, and return whatever that returns (which in this case is just 1
).
The actual type of the thing returned by std::bind(f, 1)
is some implementation-specific class type, maybe called something like std::__detail::__bind_type<void(*)(int), int>
. You're not meant to refer to that type directly, you would either capture the object using auto
or store it in something else that doesn't care about the precise type, so either:
auto f1 = std::bind(f, 1);
or:
std::function<int()> f1 = std::bind(f, 1);
In your more complex case, when you call std::bind(decltype(p_dist){0, p - 1}, std::ref(rng)))
you get a new function object that contains a copy of the temporary decltype(p_dist){0, p - 1}
and a copy of the reference_wrapper<std::mt19937>
created by std::ref(rng)
. When you invoke that new function object it will call the contained distribution, passing it a reference to rng
.
That means it is callable with no arguments, and it will call the contained random number distribution with the contained random engine, and return the result.
Q:
std::function
is helpfully referred to as 'a general-purpose polymorphic function wrapper' on cppreference.com. So is it a function that encapsulates auint64_t
return type?
A std::function<uint64_t()>
is a wrapper for a function object that is callable with no arguments and that returns a uint64_t
(or something implicitly convertible to uint64_t
). It can be used to store a copy of an arbitrary function object that is copy constructible, and callable with no arguments, and returns something convertible to uint64_t
.
(And more generally, a std::function<R(A1, A2 ... AN)>
is a wrapper for a function object that returns R
when called with N arguments, of types A1
, A2
... AN
.)
Since the result of your std::bind
call is a copy constructible function object that is callable with no arguments and returns a uint64_t
, you can store the result of that std::bind
call in a std::function<uint64_t()>
, and when you invoke the function
it will invoke the result of the bind
call, which will invoke the contained distribution, and return the result.
Or again, making use of operator () syntax to drive the notion of a function?
I'm not sure what this means, but in general it's true that in C++ we often talk about "function objects" or "callables" which are generalisations of functions, i.e. something that can be invoked using function call syntax, e.g. a(b, c)
Upvotes: 3
Reputation: 7374
I'm quoting from A Tour of C++
what is bind()
:
A function adaptor takes a function as argument and returns a function object that can be used to invoke the original function. The standard library provides
bind()
andmem_fn()
adaptors to do argument binding, also called Currying or partial evaluation. Binders were heavily used in the past, but most uses seem to be more easily expressed using lambdas.double cube(double); auto cube2 = bind(cube,2);
A call
cube2()
will invokecube
with the argument2
, that is,cube(2)
A
bind()
can be used directly, and it can be used to initialize an auto variable. In that,bind()
resembles a lambda.If we want to assign the result of
bind()
to a variable with a specific type, we can use the standard library typefunction
. Afunction
is specified with a specific return type and a specific argument type.The standard library
function
is a type that can hold any object you can invoke using the call operator()
. That is, an object of typefunction
is afunction object
.
So std::function
is a function object. A function object is used to define objects that can be called like functions.
and this is an example of function object:
template<typename T>
class Cube2 {
const T val; // value to compare against
public:
Cube2 (const T& v) :val(v) { }
double operator()(const T& x) const { return pow(x,3); } // call operator
};
And
auto cube2 = bind(cube,2);
is like saying Cube2<int> cube2{2};
Upvotes: 0
Reputation: 15085
There is no need to use std::bind
in modern C++ - use a lambda instead:
// NOTE: p and rng are captured by reference and must outlive zp_rng!
std::function<uint64_t()> zp_rng = [&]() { return decltype(p_dist){0, p - 1}(rng); };
As for std::bind and std::function instances, they are callable function objects, i.e. they have operator()
.
Upvotes: 3
Reputation: 34608
std::bind
is kind of irrelevant to std::function
, all it does is wrap something callable so that a certain set of parameters is always passed, see https://en.cppreference.com/w/cpp/utility/functional/bind
std::function
just takes anything callable which fits the function signature that you specify as its template arguments.
std::uniform_int_distribution
is callable because it has a operator()
specified, see https://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution/operator() . That specifically takes a Generator. We use bind to create a wrapper around that so that we don't always have to pass the generator explicitly. The result is then stored in the std::function
matching the uint64_t return type and no arguments (as the generator argument is bound).
If this concept is foreign to you, you should read up on operator overloading, see https://en.cppreference.com/w/cpp/language/operators (specifically Function call operator).
Upvotes: 6