Home > database >  Zero cost non-macro solution for calling a function in the correct order
Zero cost non-macro solution for calling a function in the correct order

Time:06-11

I have a code base where some common functions need to be called in order before and after some unique function

Eg:

common1();
common2();
unique();   // note: unique returns void but can have any number of arguments
common3();
common4();

A problem is, anytime a new unique is made, or anytime more commonX functions are added then every place in the code where this pattern is required has to be made to match.

One solution is to use a macro

#define DO_UNIQUE_CORRECTLY(unique_code_block) \
  common1();           \
  common2();           \
  unique_code_block    \
  common3();           \
  commont();           \

...

void someFunc1(arg1) {
  DO_UNIQUE_CORRECTLY({
    unique1(arg1);
  });
}

void someFunc2(arg1, arg2) {
  DO_UNIQUE_CORRECTLY({
    unique2(arg1, arg2);
  });
}

That works. The question is: Is there a non-Macro C way to do this with ZERO overhead AND discourage mistakes AND and not be overly verbose to use.

One solution I know of is something like this

class Helper {
  Helper() {
    common1();
    common2();
  }
  ~Helper() {
    common3();
    common4();
  }
}

void someFunc1(arg1) {
  Helper helper;
  unique1(arg1);
}

void someFunc2(arg1, arg2) {
  Helper helper;
  unique2(arg1, arg2);
}

The problem with this solution IMO is it's seems easy to insert stuff unaware that you're doing it wrong

void someFunc2(arg1, arg2) {
  Helper helper;
  doExtra1();     // bad
  unique2(arg1, arg2);
  doExtra2();     // bad
}

Like imagine that common2 is perfRecordStartTime and common3 is perfRecordEndTime. In that case you don't want anything extra before/after unique2.

You could argue the same issue with the macro

void someFunc2(arg1, arg2) {
  DO_UNIQUE_CORRECTLY({
    doExtra1();     // bad
    unique2(arg1, arg2);
    doExtra2();     // bad
  });
});

But rename DO_UNIQUE_CORRECTLY to say TIME_FUNCTION and Helper to TimingHelper

void someFunc2(arg1, arg2) {
  TIME_FUNCTION({
    doExtra1();     // clearly bad
    unique2(arg1, arg2);
    doExtra2();     // clearly bad
  });
});

vs

void someFunc2(arg1, arg2) {
  TimingHelper helper;
  doExtra1();     // bad?
  unique2(arg1, arg2);
  doExtra2();     // bad?
}

TimingHelper so doing magic stuff here. Maybe it's just an opinion but this helper pattern seems more error prone for this case than the macro pattern.

Is it also some possibly super tiny but non-zero cost calling into the Helper constructor/destructor vs the inline code of the macro?

Another is to use lambdas

template<typename Func>
void DoUniqueCorrectly(Func fn) {
  common1();
  common2();
  fn();
  common3();
  common4();
}

void someFunc1(arg1) {
  DoUniqueCorrectly([&]() {
    unique1(arg1);
  });
}

void someFunc2(arg1, arg2) {
  DoUniqueCorrectly([&]() {
    unique2(arg1, arg2);
  });
}

The problem with this one is as far as I understand, lambdas are really building an object/tuple to hold on to references to the closed over arguments. To put another way it's almost like this

void someFunc2(type1 arg1, type2 arg2)
  std::tuple<const type1&, const type2&> c = {arg1, arg2};
  DoUniqueCorrectly([&]() {
    unique2(std::get<0>(c), std::get<1>(c));
  });
}

So there is overhead of this tuple being initialized. Will that all be optimized out so the code will have the same overhead as the macro?

Let me add one more wrinkle. uniqueX can be a more complex function call. For example

void SomeClass::someFunc2(arg1, arg2) {
  DO_UNIQUE_CORRECTLY({
    getContext()->getThing()->unique2(arg1, arg2);
  });
}

Is there another solution?

PS: I get that if any overhead exists it's small. I still want to know if there is a zero overhead non-macro C way of doing this.

CodePudding user response:

The decorator you presented can be easily extended to pass an arbitrary amount of arguments to your invocable:

template<typename Func, class... Args>
void DoUniqueCorrectly(Func&& fn, Args&&... args) 
{
  common1();
  common2();
  std::invoke(std::forward<Func>(fn), std::forward<Args>(args)...);
  common3();
  common4();
}

then your use cases are called as:

void someFunc1(arg1) { DoUniqueCorrectly(unique1, arg1); }
void someFunc2(arg1, arg2) { DoUniqueCorrectly(unique2, arg1, arg2); }

Everything forwarded to an inlinable function template (DoUniqueCorrectly). No extra or intermediate objects. Plus std::invoke will "generate" the correct call syntax for any type of callable (function pointers, lambdas, pointers to member functions etc etc) something that a MACRO doesn't do out of the box.

The ammended example (the wrinkle) would be called as

DoUniqueCorrectly(&Thing::unique2, o.getContext()->getThing(), 1, 2);

i.e. pointer to member function called from object pointer. std::invoke has no problem.

Demo

CodePudding user response:

You are on the right track there with the Helper. What you missed in your example is a scope block so the destructor isn't delayed:

{
    Helper helper;
    unique1(arg1);
}

As you say it's easy to insert other code into the block or forget the block entirely. One solution is to pass the unique function to the helper to execute. (or make it a function instead of an object):

Helper helper(&unique1, arg1);
helper_fn(&unique1, arg1);

Now the helper can run everything in the constructor (or the function) and nothing can be inserted. But it's not quite readable. And maybe you don't have a function to call by name but want to just run some code. You can do that with a lambda as you mentioned yourself:

Helper helper([&]() { some; code; });

That almost looks like other code constructs you have like while (cond) { some; code; }. Can we make it look even more like that? How about giving Helper an operator that takes a callable? Then you can write:

Helper()   [&]() { some; code; }

For a last improvement I'm afraid we are back to using macros:

#define UNIQUE Helper()   [&]()

UNIQUE { some; code; }

I think that's the cleanest syntax you can get.

As for the overhead of the lambda: In simple cases the compiler should optimize it all away. In complex cases the overhead is tiny compared to the time the complex case takes. If it would take no time it would be a simple case the compiler optimizes. Try it and see how complex a case you have to make before it doesn't get optimized away. It's hard to predict what the compiler will do exactly, you have to test it.

For a full example of this method of hiding the lambda with proper exception handling look at Declarative Control Flow. This actually delays code to be run at the end of the scope with the options to always run it or only on success or error. But you can easily adapt it to your use case and being able to handle errors in your UNIQUE helper might be invaluable.

CodePudding user response:

Here’s a template-based solution where no additional parameters are passed (lambdas, std::functions or other) and everyting can be optimized down to “bare C“ by the compiler. It can be also easily extended and specialized for a unique() with a (non-void) return type if need be, but this is left out for the sake of brevity.

#include <iostream>
#include <utility>

namespace {
using std::cout;
using std::forward;

void before() { cout << "before\n"; }
void after() { cout << "after\n"; }

void unique0() { cout << "unique0()\n"; }
void unique1(int a) { cout << "unique1(" << a << ")\n"; }
void unique2(int a, int b) { cout << "unique2(" << a << ", " << b << ")\n"; };

template <typename... A>
struct DoUniqueCorrectly {
  template <void (*U)(A...), typename... AA>
  static void unique(AA&&... args) {
    before();
    U(forward<AA>(args)...);
    after();
  }
};
}  // namespace

int main() {
  DoUniqueCorrectly<>::unique<&unique0>();
  DoUniqueCorrectly<int>::unique<&unique1>(3);
  DoUniqueCorrectly<int, int>::unique<&unique2>(1, 2);
}
  •  Tags:  
  • c
  • Related