Bob Fang
Bob Fang

Reputation: 7411

Could I use template to control block of code and make sure it does not explode?

I am writing a function that does some computation on a large list of table data in C++. The function should take in a list of "instructions" to specify what to be computed.

For example the table can look like this

| A | B | C |
|---|---|---|
| 1 | 2 | 4 |
| 2 | 3 | 5 |
| 3 | 5 | 6 |
|...........|

I am trying to build a function that look like this

std::vector<int> compute(const Table& input, const std::vector<MetricsEnum>& metrics)
{
    std::vector<int> result;
    result.reserve(some heuristic number I put in);
    for(const auto& row: input)
    {
       if(std::find(metrics.begin(), metrics.end(), Metrics1) != metrics.end())
       {
           result.push_back(row[A] + row[B]); 
       }
      if(std::find(metrics.begin(), metrics.end(), Metrics2) != metrics.end())
      {
          result.push_back(row[A] - row[B]); 
      }
      // this list goes for about 5 or 6 metrics for now, but later I plan to add more
    }
}

So the probelm I am having is that, Apparently the input has many rows, and doing the if statement inside the loop is more like a luxury for me now. I want to be able to use template to generates a bunch of functions at compile time and choose one of them based on what metrics I want at run time. Something on the line with:

template <bool metrics1, bool metrics2 ...>
std::vector compute(const Table& input>
{
    ...
    if(metrics1)
    {
        result.push_back(row[A] + row[B]);
    }
    if(metrics2)
    {
        result.push_back(row[A] - row[B]);
    }
    ...
}

But there is a couple of problems here which I find it very hard for me:

  1. I am not sure how could I implement my idea here. Could you point me to some examples? I have a vague feeling that if constexpr in C++17 can do me good. But unluckily I am still in C++11 land and have to stay here for a while.
  2. More importantly, is my idea worth implementing at all? As I understand the C++ compiler will generate 2^n functions at compile time, where n is number of metrics. For now the number is relatively small, but the number of functions grows exponentially, and I am pretty sure n will get larger than 10 at some point. So my question is should I be worried that my binary file will explode in this case?

Upvotes: 0

Views: 149

Answers (2)

Jarod42
Jarod42

Reputation: 217990

More importantly, is my idea worth implementing at all? As I understand the C++ compiler will generate 2^n functions at compile time, where n is number of metrics.

Correct.

What you may do instead is to compute metric by type and place result at correct place, something like:

template <typename F>
void loop(const Table& input, std::size_t blockSize,
          std::size_t& offset, std::vector<int>& result,
          F f)
{
    for (std::size_t i = 0; i != input.size(); ++i) {
        const auto& row = input[i];
        result[blockSize * i + offset] = f(row);
    }
    ++offset;
}

std::vector<int> compute(const Table& input, const std::vector<MetricsEnum>& metrics)
{
    std::vector<int> result(input.size() * metrics.size());
    std::size_t offset = 0;

    if (std::find(metrics.begin(), metrics.end(), Metrics1) != metrics.end()) {
        loop(input, metrics.size(), offset, result,
             [](const Row& row) { return row[A] + row[B]; });
    }
    if (std::find(metrics.begin(), metrics.end(), Metrics2) != metrics.end()) {
        loop(input, metrics.size(), offset, result,
             [](const Row& row) { return row[A] - row[B]; });
    }
    // this list goes for about 5 or 6 metrics for now, but later I plan to add more      

    return result;
}

Upvotes: 0

Xirema
Xirema

Reputation: 20396

Forget trying to do things at compile-time, and (for now) forget about performance. Optimize that part once you have the actual functionality figured out.

In this case, what you [appear to be trying to] do is, for each row in your table, evaluate a series of computations on two predetermined indexes, and push the result into a vector. I don't know how A or B get their values, so my solution isn't going to concern them.

My first suggestion would be to organize the whole thing into a table of functions that can be called:

//Replace all instances of 'int' with whatever type you're using
std::vector<int> compute(const Table& input, const std::vector<MetricsEnum>& metrics)
{
    typedef int (*func_ptr_type)(int,int);
    //I'm assuming MetricsEnum is a literal enum type, convertible to an integer.
    static const std::array<func_ptr_type, 6> functions{
        +[](int a, int b) {return a + b;},
        +[](int a, int b) {return a - b;},
        +[](int a, int b) {return a * b;},
        +[](int a, int b) {return a / b;},
        +[](int a, int b) {return a << b;},
        +[](int a, int b) {return a >> b;}
        //Add more and increase the size of the array, as needed
    };
    std::vector<int> result;
    //Don't do this; let the compiler and allocator do their jobs
    //result.reserver(some heuristic number I put in);
    for(const auto& row: input)
    {
        for(MetricsEnum metricsEnum : metrics) {
            result.emplace_back(functions.at(size_t(metrics))(row[A], row[B]));
        }
    }
    return result;
}

In this form, it's much easier to see what it is the code is meant to be doing, and we also see that it's much easier to keep the whole thing organized.

The next step would be to eliminate the array altogether and make the function a core part of the behavior of the MetricsEnum type, whatever that is.

template<typename T>
class MetricsEnum {
public:
    enum class Type {
        add, subtract, multiply, divide, shift_left, shift_right
    };
    constexpr MetricsEnum(Type type) : type(type) {}

    constexpr T operator()(T a, T b) const {
        switch(type) {
            case Type::add: return a + b;
            case Type::subtract: return a - b;
            case Type::multiply: return a * b;
            case Type::divide: return a / b;
            case Type::shift_left: return a << b;
            case Type::shift_right: return a >> b;
            default: return {};
        }
    }
private:
    Type type;
};

std::vector<int> compute(const Table& input, const std::vector<MetricsEnum<int>>& metrics)
{
    std::vector<int> result;
    for(const auto& row: input)
    {
        for(auto const& metricsEnum : metrics) {
            result.emplace_back(metricsEnum(row[A], row[B]));
        }
    }
    return result;
}

There are any number of other ways this could be handled (polymorphism comes to mind...); this is most intuitive to me.

Upvotes: 1

Related Questions