Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading