1

I have a couple of boiler-plate functions I would like to replace with a template. They look roughly like:

std::vector<double> generate_means(
  std::mt19937& g, unsigned int N,
  double lower = -1.0, double upper = 1.0
) {
  std::uniform_real_distribution<double> dist(lower, upper);
  std::function<double()> rng = std::bind(dist, g);
  std::vector<double> res(N);
  std::generate(std::begin(res), std::end(res), gen);
  return res;
}

The elements that need abstraction are return type (only contained type, always vector is fine) the arguments after N (e.g., lower and upper in this case) and the distribution (e.g., std::uniform_real_distribution).

What I'd like roughly to be able write:

auto generate_means = generate_template<
  double, // results vector<double>
  std::uniform_real_distribution, // uses uniform distro
  double=-1.0,double=1.0 // with default args
>
auto generate_norm_deviates = generate_template<
  double, // still provides vector<double>
  std::normal_distribution, // different distro
  double=0, double=1.0 // different defaults
>
auto generate_category_ids = generate_template<
  unsigned int,
  std::uniform_int_distribution,
  unsigned int=0, unsigned int // again with two args, but only one default
>

I have some sub-pieces

template <class NUMERIC>
using generator = std::function<NUMERIC()>;

template <class NUMERIC>
std::vector<NUMERIC> series(unsigned int length, generator<NUMERIC> gen) {
  std::vector<NUMERIC> res(length);
  std::generate(std::begin(res), std::end(res), gen);
  return res;
};

but when I try assembling like, for example

template <class NUMERIC, class DIST, class...Args>
std::vector<NUMERIC> generator_template(
  std::mt19937& g, unsigned int N,
  Args... args
) {
  DIST<NUMERIC> dist(&args...);
  generator<NUMERIC> gen = std::bind(dist, g);
  return series(N, gen);
} 

I run into compile errors (in this case error: expected unqualified-id). Is what I'd like approximately achievable? Is this approach in the right direction, or do I need do something fundamentally different? If it is in the right direction, what am I missing?

EDIT:

For application constraints: I'd like to be able to declare the generators with defaults for arguments, but I do need to occasionally use them without the defaults. Not having defaults is just inconvenient, however, not fatal. Example:

//... assorted calculations...
auto xmeans = generate_means(rng, 100); // x on (-1,1);
auto ymeans = generate_means(rng, 100); // y on (-1,1);
auto zmeans = generate_means(rng, 100, 0, 1); // z on (0,1);
Carl
  • 7,538
  • 1
  • 40
  • 64
  • Not the answer, but I suggest not storing the result of `std::bind` in a `std::function`. If you use `auto` instead, you avoid the overhead of type erasure that `std::function` adds. – HolyBlackCat Jan 16 '19 at 11:55
  • 1
    "&args..." is not how you expand a parameter pack, in this instance. Try `std::forward(args)...`. And the template parameters should be `Args ... &&args`. There may be other issues here, but this is the first obvious one. – Sam Varshavchik Jan 16 '19 at 11:56
  • @HolyBlackCat yep, learned that earlier trying to build-up some of the other pieces. – Carl Jan 16 '19 at 11:57
  • 1
    Also, it has to be `class ... Args` in the template list, not just `Args...`. – Max Langhof Jan 16 '19 at 11:57
  • "like, for example"? Why don't you post the exact code you tried instead so we don't get silly "‘Args’ has not been declared" kind of errors? – user202729 Jan 16 '19 at 11:58
  • apologies re the `Args` part - had fixed that in earlier version of Q, must have gotten lost in edits. – Carl Jan 16 '19 at 11:59
  • @SamVarshavchik I think it's `Args &&... args`. – HolyBlackCat Jan 16 '19 at 11:59
  • 1
    [Template template parameters](https://stackoverflow.com/questions/213761/what-are-some-uses-of-template-template-parameters) – user202729 Jan 16 '19 at 11:59
  • The default args part is unclear. You can use template parameters to specify a type **or** a compile-time **integral** value. It seems you are trying to do both at once here (and with doubles), which won't work. You **can** specify all argument types for the distribution. With some extra work, you might also be able to include default arguments for integral values, but you cannot really communicate floating point values with templates. – Max Langhof Jan 16 '19 at 12:04
  • 1
    [How do I assign an alias to a function name in C++? - Stack Overflow](https://stackoverflow.com/questions/3053561/how-do-i-assign-an-alias-to-a-function-name-in-c) – user202729 Jan 16 '19 at 12:08
  • Do you want the `args` part of template parameters, or passed as function input? – user202729 Jan 16 '19 at 12:13
  • Is the type of elements of `args` the same as `NUMERIC`? – user202729 Jan 16 '19 at 12:14
  • @user202729 not necessarily - they are whatever the distribution functions use as parameters. Re template parameters vs args, I'm not precisely sure I understand q - I ultimately use the functions like `auto xmeans = generate_means(rng, 100, 0, 1), ymeans = generate_means(rng, 100, 0, 1);` etc. I would like to be able to call `generate_means(rng, 100)` with defaults set when instantiating function from template, but still allowing occasional use with non defaults. – Carl Jan 16 '19 at 12:21

3 Answers3

2

It is impossible to have floating point number as template parameter. However, you can do something as follow :

#include <random>
#include <limits>
#include <algorithm>
#include <vector>
#include <iostream>

template<typename T, template<typename> typename Distribution>
auto generate_random_template(T min = std::numeric_limits<T>::lowest(),
                              T max = std::numeric_limits<T>::max()) {
    return [distribution = Distribution<double>{min, max}]
        (auto &&generator, std::size_t number) mutable {
        std::vector<T> result;
        result.reserve(number);
        auto generate = [&](){return distribution(generator);};
        std::generate_n(std::back_inserter(result), number, generate);
        return result;
    };
}

int main() {
    auto generate_means = generate_random_template<double, std::uniform_real_distribution>(0.0, 1.0);
    std::mt19937 g;
    std::vector<double> randoms = generate_means(g, 10);

    for(auto r : randoms) std::cout << r << std::endl;

    return 0;
}

EDIT: Use generate_n instead of generate for performances reasons

EDIT2 : If you want to use default parameters like you did for x, y, and z, you can also do something like that :

#include <random>
#include <limits>
#include <algorithm>
#include <vector>
#include <iostream>

template<typename T, template<typename> typename Distribution>
auto generate_random_template(T min = std::numeric_limits<T>::lowest(),
                              T max = std::numeric_limits<T>::max()) {
    return [distribution = Distribution<double>{min, max}, min, max]
        (auto &&generator, std::size_t number, auto ...args) mutable {
        std::vector<T> result;
        result.reserve(number);

        if constexpr(sizeof...(args) > 0)
            distribution.param(typename Distribution<T>::param_type(args...));

        else
            distribution.param(typename Distribution<T>::param_type(min, max));

        auto generate = [&](){return distribution(generator);};
        std::generate_n(std::back_inserter(result), number, generate);
        return result;
    };
}

int main() {
    auto generate_means = generate_random_template<double, std::uniform_real_distribution>(-1.0, 1.0);
    std::mt19937 g;
    // x and y are between -1 and 1
    std::vector<double> x = generate_means(g, 10);
    std::vector<double> y = generate_means(g, 10);
    std::vector<double> z = generate_means(g, 10, 0.0, 1.0); // z is between 0 and 1

    for(int i = 0; i < 10; ++i) {
        std::cout << x[i] << "," << y[i] << "," << z[i] << std::endl;   
    }
    return 0;
}
Antoine Morrier
  • 3,930
  • 16
  • 37
0

Thanks assorted commenters, this is now working (when added to working blocks from Q):

template <class NUMERIC, template<class> class DIST, class ... Args>
std::vector<NUMERIC> generator_template(
  std::mt19937& g, unsigned int N,
  Args &&... args
) {
  DIST<NUMERIC> dist(std::forward<Args>(args)...);
  generator<NUMERIC> gen = std::bind(dist, g);
  return series(N, gen);
};

auto generate_test = generator_template<double, std::uniform_real_distribution, double, double>;

Happy to see other answers, however - still trying to understand C++ template syntax generally, and would prefer a version that let's me set default arguments.

Carl
  • 7,538
  • 1
  • 40
  • 64
0

I'd be tempted to just accept a constructed distribution object.

template <typename Dist, typename URBG>
std::vector<typename Dist::value_type> generate(Dist&& dist, URBG&& gen, std::size_t N)
{
    std::vector<typename Dist::value_type> res(N);
    std::generate(res.begin(), res.end(), std::bind(std::forward<Dist>(dist), std::forward<URBG>(gen)));
    return res;
}
Caleth
  • 52,200
  • 2
  • 44
  • 75