Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid run-time checks for running parts of code that become unreachable after compilation?

Tags:

c++

My program gets a couple of Boolean variables from the user, and their values won't change afterwards. Each Boolean variable enables a part of code. Something like this:

#include <iostream>

void callback_function(bool task_1, bool task_2, bool task_3) {
  if (task_1) {
    std::cout << "Running task 1" << std::endl;
  }
  if (task_2) {
    std::cout << "Running task 2" << std::endl;
  }
  if (task_3) {
    std::cout << "Running task 3" << std::endl;
  }
}

int main() {
  bool task_1 = true;
  bool task_2 = false;
  bool task_3 = true;

  while (true) {
    callback_function(task_1, task_2, task_3);
  }

  return 0;
}

Now my question is, since the Boolean variables are fixed every time the program calls callback_function(), is there a way to avoid the if statements inside the callback function?

This is one way to avoid the run-time checks (implement a callback function for all permutations of the Boolean variables --- only two cases are shown below):

#include <functional>
#include <iostream>

void callback_function_for_tasks_1_2_3() {
  std::cout << "Running task 1" << std::endl;
  std::cout << "Running task 2" << std::endl;
  std::cout << "Running task 3" << std::endl;
}

void callback_function_for_tasks_1_3() {
  std::cout << "Running task 1" << std::endl;
  std::cout << "Running task 3" << std::endl;
}

int main() {
  bool task_1 = true;
  bool task_2 = false;
  bool task_3 = true;

  std::function<void()> callback_function;
  if (task_1 && task_2 && task_3) {
    callback_function = callback_function_for_tasks_1_2_3;
  } else if (task_1 && !task_2 && task_3) {
    callback_function = callback_function_for_tasks_1_3;
  }

  while (true) {
    callback_function();
  }

  return 0;
}

The problem is I have to implement 2^n different callback functions, if there are n Boolean variables. Is there a better way to accomplish this?

like image 343
Alireza Shafaei Avatar asked Mar 31 '19 05:03

Alireza Shafaei


1 Answers

Ensuring that if statements are evaluated at compile time

C++17 introduces if constexpr, which does exactly this:

template<bool task_1, bool task_2, bool task_3>
void callback_function() {
  if constexpr (task_1) {
    std::cout << "Running task 1" << std::endl;
  }
  if constexpr (task_2) {
    std::cout << "Running task 2" << std::endl;
  }
  if constexpr (task_3) {
    std::cout << "Running task 3" << std::endl;
  }
}

If you have optimizations enabled, if constexpr isn't necessary. Even if you use a regular if instead of if constexpr, because the bools are now templated, the compiler will be able to eliminate the if statements entirely, and just run the tasks. If you look at the assembly produced here, you'll see that even at -O1, there are no if statements in any of the callback functions.

We can now use callback_function directly as a function pointer, avoiding function<void()>:

int main() {
  using callback_t = void(*)();
  callback_t func = callback_function<true, false, true>;

  // Do stuff with func 
}

We can also name the bools by assigning them to constexpr variables:

int main() {
  using callback_t = void(*)();
  constexpr bool do_task1 = true;
  constexpr bool do_task2 = false;
  constexpr bool do_task3 = true; 
  callback_t func = callback_function<do_task1, do_task2, do_task3>;

  // Do stuff with func 
}

Automatically creating a lookup table of all possible callback functions

You mentioned choosing between different callback functions at runtime. We can do this pretty easily with a lookup table, and we can use templates to automatically create a lookup table of all possible callback functions.

The first step is to get a callback function from a particular index:

// void(*)() is ugly to type, so I alias it
using callback_t = void(*)();

// Unpacks the bits 
template<size_t index>
constexpr auto getCallbackFromIndex() -> callback_t 
{
    constexpr bool do_task1 = (index & 4) != 0;
    constexpr bool do_task2 = (index & 2) != 0;
    constexpr bool do_task3 = (index & 1) != 0; 
    return callback_function<do_task1, do_task2, do_task3>; 
}

Once we can do that, we can write a function to create a lookup table from a bunch of indexes. Our lookup table will just be a std::array.

// Create a std::array based on a list of flags
// See https://en.cppreference.com/w/cpp/utility/integer_sequence
// For more information
template<size_t... Indexes>
constexpr auto getVersionLookup(std::index_sequence<Indexes...>) 
    -> std::array<callback_t, sizeof...(Indexes)>
{
    return {getCallbackFromIndex<Indexes>()...}; 
}

// Makes a lookup table containing all 8 possible callback functions
constexpr auto callbackLookupTable = 
    getVersionLookup(std::make_index_sequence<8>()); 

Here, callbackLookupTable contains all 8 possible callback functions, where callbackLookupTable[i] expands the bits of i to get the callback. For example, if i == 6, then i's bits are 110 in binary, so

callbackLookupTable[6] is callback_function<true, true, false>

Using the lookup table at runtime

Using the lookup table is really simple. We can get an index from a bunch of bools by bitshifting:

callback_t getCallbackBasedOnTasks(bool task1, bool task2, bool task3) {
    // Get the index based on bit shifting
    int index = ((int)task1 << 2) + ((int)task2 << 1) + ((int)task3); 
    // return the correct callback
    return callbackLookupTable[index]; 
}

Example demonstrating how to read in tasks

We can get the bools at runtime now, and just call getCallbackBasedOnTasks to get the correct callback

int main() {
    bool t1, t2, t3;
    // Read in bools
    std::cin >> t1 >> t2 >> t3; 
    // Get the callback
    callback_t func = getCallbackBasedOnTasks(t1, t2, t3); 
    // Invoke the callback
    func(); 
}
like image 148
Alecto Irene Perez Avatar answered Sep 24 '22 21:09

Alecto Irene Perez