How to fix class function behaviour based on initialization parameters

Viewed 82

I am creating a C++ class which takes certain parameters during initialization and has some functions based on its private variables, something like the compute function here:

class A {
  public:
    A(int x){
      a = x;
    }
    int compute(int y){
      if (a == 0){
        return y*y;
      }
      else if (a == 1){
        return 2*y;
      }
      else{
        return y;
      }
    }
  private:
    int a;
};

// usage

A myA(1); // private variables set only once
myA.compute(10); // this will check value of a 
myA.compute(1); // this will check value of a

Given that the private variables are set during initialization and will not be changed again, is there any efficient way to avoid the condition check related to the private variables during runtime?

Any and all assistance is appreciated. Thank you

4 Answers

You could avoid the condition check if you would use e.g. a function object as a member, and set this conditioned on the value of variable a. Anyway, I don't think that the condition check will be big performance issue. But this will depend on your application of course.

#include <functional>
#include <iostream>

class A {
  public:
    A(int x)
    : a { x } 
    {
      if (a == 0){
        compute = [](int y){ return y*y; };
      }
      else if (a == 1){
        compute = [](int y){ return 2*y; };
      }
      else{
        compute = [](int y){ return y; };
      }

    }

    
    std::function<int(int)> compute;
    
  private:
    int a;
};

// usage


int main()
{
 
    A myA(1); // private variables set only once
    std::cout << myA.compute(10) << std::endl;
    std::cout << myA.compute(1) << std::endl;
    return 0;
}

You can template the function compute() on an int and use the template value as parameter. You can see the result at https://godbolt.org/z/14Mh4E

class A {
public:
    A(int x) {
        a = x;
    }
    template <int y>
    constexpr int compute() const {
        if (a == 0) {
            return y * y;
        }
        else if (a == 1) {
            return 2 * y;
        }
        else {
            return y;
        }
    }
private:
    int a;
};

// usage

A myA(1); // private variables set only once
myA.compute<10>(); // this will check value of a 
myA.compute<1>(); // this will check value of a

You can guarantee the conditions are evaluated at compile time by using constexpr. Note that in this case you must use C++14 for constexpr compute(...), as multiple return statements are only suppoerted in constexpr functions after C++14.

#include <iostream>

class A {
  public:
    constexpr A(const int x): a(x) { }
    constexpr int compute(const int y) const {
      // Multiple return statements inside a constexpr function
      // requires C++14 or above.
      if (a == 0) {
        return y*y;
      }
      else if (a == 1) {
        return 2*y;
      }
      else {
        return y;
      }
    }
  private:
    int a;
};


int main() {
  constexpr A myA(1);
  constexpr int num = myA.compute(123);

  std::cout << num << std::endl;

  return EXIT_SUCCESS;
}

This page contains a good explanation of constexpr, as well as examples.

If parameters are runtime value, I don't see an optimal way to avoid condition or jump.

You can trade your condition by virtual call:

struct A
{
    virtual ~A() = default;
    virtual int compute(int) = 0;
};

struct A0 { int compute(int y) override { return y * y; } };
struct A1 { int compute(int y) override { return 2 * y; } };
struct AN { int compute(int y) override { return y; } };

std::unique_ptr<A> makeA(int a)
{
    switch (a) {
        case 0: return std::make_unique<A0>();
        case 0: return std::make_unique<A1>();
        default: return std::make_unique<AN>();
    }
}

(compiler might devirtualize the call if type is known at compile time)

or "equivalent":

struct A
{
    int (*f)(int); // or even std::function<int(int)> f; if you need capture.

    A(int a) : f(a == 0 ? +[](int y) { return y * y; }
               : a == 1 ? +[](int y) { return 2 * y; }
                        : +[](int y) { return y; })
    {}

    int compute(int y) { return f(y); }
};

(erased-type is harder for compiler to devirtualize)

Related