Carl
Carl

Reputation: 7554

C++ template function - multiple types, default and ... arguments?

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);

Upvotes: 1

Views: 2908

Answers (3)

Caleth
Caleth

Reputation: 62694

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;
}

Upvotes: 0

Antoine Morrier
Antoine Morrier

Reputation: 4078

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;
}

Upvotes: 2

Carl
Carl

Reputation: 7554

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.

Upvotes: 0

Related Questions