Duplizieren Sie Code mit c ++ 11

80

Ich arbeite derzeit an einem Projekt und habe das folgende Problem.

Ich habe eine C ++ - Methode, mit der ich auf zwei verschiedene Arten arbeiten möchte:

void MyFunction()
{
  foo();
  bar();
  foobar();
}

void MyFunctionWithABonus()
{
  foo();
  bar();
  doBonusStuff();
  foobar();
}

Und ich möchte meinen Code nicht duplizieren, da die eigentliche Funktion viel länger ist. Das Problem ist, dass ich unter keinen Umständen dem Programm Ausführungszeit hinzufügen darf, wenn MyFunction anstelle von MyFunctionWithABonus aufgerufen wird. Deshalb kann ich nicht einfach einen booleschen Parameter haben, den ich mit einem C ++ - Vergleich überprüfe.

Meine Idee wäre gewesen, C ++ - Vorlagen zu verwenden, um meinen Code virtuell zu duplizieren, aber ich kann mir keine Möglichkeit vorstellen, bei der ich keine zusätzliche Ausführungszeit habe und den Code nicht duplizieren muss.

Ich bin kein Experte für Vorlagen, daher fehlt mir möglicherweise etwas.

Hat jemand von euch eine Idee? Oder ist das in C ++ 11 einfach unmöglich?

Plougue
quelle
64
Darf ich fragen, warum Sie nicht einfach einen Booleschen Scheck hinzufügen können? Wenn dort viel Code enthalten ist, ist der Aufwand für eine einfache boolesche Prüfung vernachlässigbar.
Joris
39
@plougue Die Verzweigungsvorhersage ist heutzutage sehr gut, bis zu dem Punkt, dass eine boolesche Prüfung häufig 0 Prozessorzyklen benötigt, um ausgeführt zu werden.
Dan
4
Stimmen Sie mit @Dan überein. Die Vorhersage von Zweigen kostet heutzutage fast null Overhead, insbesondere wenn Sie einen bestimmten Zweig häufig betreten.
Akshay Arora
6
@Dan: Ein Compare-and-Branch ist immer noch bestenfalls ein makroverschmolzenes UOP (auf modernen Intel- und AMD x86- CPUs), nicht Null. Abhängig davon, was der Engpass in Ihrem Code ist, kann das Dekodieren / Ausgeben / Ausführen dieses UOP einen Zyklus von etwas anderem stehlen, genauso wie es ein zusätzlicher ADD-Befehl tun könnte. Das Übergeben des booleschen Parameters und das Binden eines Registers (oder das Verschütten / Neuladen) ist eine Anzahl von Anweisungen ungleich Null. Hoffentlich ist diese Funktion inline, so dass der Overhead für Anrufe und Argumente nicht jedes Mal vorhanden ist, und vielleicht cmp + branch, aber immer noch
Peter Cordes
15
Haben Sie den Code zuerst im einfach zu wartenden Format geschrieben? Hat Ihr Profiler dann gesagt, dass die Branche der Engpass ist? Haben Sie Daten, die darauf hindeuten, wie viel Zeit Sie für diese kleine Entscheidung aufwenden, um Ihre Zeit optimal zu nutzen?
GManNickG

Antworten:

55

Mit Vorlage und Lambda können Sie Folgendes tun:

template <typename F>
void common(F f)
{
  foo();
  bar();
  f();
  foobar();
}

void MyFunction()
{
    common([](){});
}

void MyFunctionWithABonus()
{
  common(&doBonusStuff);
}

oder Sie können einfach erstellen prefixund suffixfunktionieren.

void prefix()
{
  foo();
  bar();
}

void suffix()
{
    foobar();
}

void MyFunction()
{
    prefix();
    suffix();
}

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}
Jarod42
quelle
12
Ich bevorzuge diese beiden Lösungen tatsächlich gegenüber einem booleschen Parameter (Vorlage oder auf andere Weise), unabhängig von den Vorteilen der Ausführungszeit. Ich mag keine booleschen Parameter.
Chris Drew
2
Nach meinem Verständnis wird die zweite Lösung aufgrund eines zusätzlichen Funktionsaufrufs eine zusätzliche Laufzeit haben. Ist dies beim ersten der Fall? Ich bin nicht sicher, wie Lambdas in diesem Fall
funktionieren
10
Wenn Definitionen sichtbar sind, würde der Compiler wahrscheinlich Code einfügen und denselben Code generieren, der für Ihren ursprünglichen Code generiert wurde.
Jarod42
1
@ Yakk Ich denke, es wird vom jeweiligen Anwendungsfall abhängen und wessen Verantwortung das "Bonusmaterial" ist. Oft finde ich, dass Bool-Parameter, Ifs und Bonus-Inhalte im Hauptalgorithmus das Lesen erschweren und es vorziehen, dass sie "nicht mehr existieren" und von anderen Stellen eingekapselt und injiziert werden. Ich denke jedoch, dass die Frage, wann es angemessen ist, das Strategiemuster zu verwenden, wahrscheinlich den Rahmen dieser Frage sprengt.
Chris Drew
2
Die Tail-Call-Optimierung ist normalerweise wichtig, wenn Sie rekursive Fälle optimieren möchten. In diesem Fall macht einfaches Inlining ... alles, was Sie brauchen.
Yakk - Adam Nevraumont
128

So etwas wird gut funktionieren:

template<bool bonus = false>
void MyFunction()
{
  foo();
  bar();
  if (bonus) { doBonusStuff(); }
  foobar();
}

Nennen Sie es über:

MyFunction<true>();
MyFunction<false>();
MyFunction(); // Call myFunction with the false template by default

Die "hässliche" Vorlage kann vermieden werden, indem den Funktionen einige nette Wrapper hinzugefügt werden:

void MyFunctionAlone() { MyFunction<false>(); }
void MyFunctionBonus() { MyFunction<true>(); }

Sie können einige nette Informationen auf dieser Technik finden dort . Das ist ein "altes" Papier, aber die Technik an sich bleibt völlig richtig.

Vorausgesetzt, Sie haben Zugriff auf einen netten C ++ 17-Compiler, können Sie die Technik sogar weiter vorantreiben, indem Sie den constexpr verwenden, wenn :

template <int bonus>
auto MyFunction() {
  foo();
  bar();
  if      constexpr (bonus == 0) { doBonusStuff1(); }
  else if constexpr (bonus == 1) { doBonusStuff2(); }
  else if constexpr (bonus == 2) { doBonusStuff3(); }
  else if constexpr (bonus == 3) { doBonusStuff4(); }
  // Guarantee that this function will not compile
  // if a bonus different than 0,1,2,3 is passer
  else { static_assert(false);}, 
  foorbar();
}
Gibet
quelle
11
Und diese Prüfung wird vom Compiler
Jonas
21
Und in C ++ 17 if constexpr (bonus) { doBonusStuff(); } .
Chris Drew
5
@ChrisDrew Ich bin mir nicht sicher, ob der Constexpr hier etwas hinzufügen würde. Würde es?
Gibet
13
@ Gibet: Wenn der Aufruf von doBonusStuff()aus irgendeinem Grund im Nicht-Bonus-Fall nicht einmal kompiliert werden kann, wird dies einen großen Unterschied machen.
Leichtigkeitsrennen im Orbit
4
@WorldSEnder Ja, Sie könnten, wenn Sie mit Aufzählungen oder Aufzählungsklassen constexpr meinen (Bonus == MyBonus :: ExtraSpeed).
Gibet
27

In Anbetracht einiger Kommentare, die das OP zum Debuggen abgegeben hat, ist hier eine Version, die doBonusStuff()Debug-Builds fordert , aber keine Builds veröffentlicht (die definieren NDEBUG):

#if defined(NDEBUG)
#define DEBUG(x)
#else
#define DEBUG(x) x
#endif

void MyFunctionWithABonus()
{
  foo();
  bar();
  DEBUG(doBonusStuff());
  foobar();
}

Sie können das assertMakro auch verwenden, wenn Sie eine Bedingung überprüfen möchten, und fehlschlagen, wenn sie falsch ist (jedoch nur für Debug-Builds; Release-Builds führen die Prüfung nicht durch).

Seien Sie vorsichtig, wenn doBonusStuff()Nebenwirkungen auftreten, da diese Nebenwirkungen in Release-Builds nicht vorhanden sind und möglicherweise die im Code getroffenen Annahmen ungültig machen.

Maisstängel
quelle
Die Warnung vor Nebenwirkungen ist gut, aber es ist auch wahr, egal welches Konstrukt verwendet wird, seien es Vorlagen, wenn () {...}, constexpr usw.
Pipe
Angesichts der OP-Kommentare habe ich dies selbst positiv bewertet, da es genau die beste Lösung für sie ist. Das heißt, nur eine Kuriosität: Warum all die Komplikationen mit den neuen Definitionen und alles, wenn Sie den Aufruf von doBonusStuff () einfach in ein #if defined (NDEBUG) einfügen können?
MotoDrizzt
@motoDrizzt: Wenn das OP dasselbe in anderen Funktionen tun möchte, finde ich die Einführung eines neuen Makros wie dieses sauberer / einfacher zu lesen (und zu schreiben). Wenn es nur eine einmalige Sache ist, dann stimme ich zu, dass #if defined(NDEBUG)es wahrscheinlich einfacher ist , nur direkt zu verwenden.
Cornstalks
@Cornstalks yep, es macht total Sinn, ich habe nicht so weit darüber nachgedacht. Und ich denke immer noch, dass dies die akzeptierte Antwort sein sollte :-)
motoDrizzt
18

Hier ist eine geringfügige Abweichung von Jarod42s Antwort unter Verwendung verschiedener Vorlagen, sodass der Anrufer null oder eine Bonusfunktion bereitstellen kann:

void callBonus() {}

template<typename F>
void callBonus(F&& f) { f(); }

template <typename ...F>
void MyFunction(F&&... f)
{
  foo();
  bar();
  callBonus(std::forward<F>(f)...);
  foobar();
}

Aufrufcode:

MyFunction();
MyFunction(&doBonusStuff);
Chris Drew
quelle
11

Eine andere Version, die nur Vorlagen und keine Umleitungsfunktionen verwendet, da Sie sagten, Sie wollten keinen Laufzeit-Overhead. Für mich erhöht dies nur die Kompilierungszeit:

#include <iostream>

using namespace std;

void foo() { cout << "foo\n"; };
void bar() { cout << "bar\n"; };
void bak() { cout << "bak\n"; };

template <bool = false>
void bonus() {};

template <>
void bonus<true>()
{
    cout << "Doing bonus\n";
};

template <bool withBonus = false>
void MyFunc()
{
    foo();
    bar();
    bonus<withBonus>();
    bak();
}

int main(int argc, const char* argv[])
{
    MyFunc();
    cout << "\n";
    MyFunc<true>();
}

output:
foo
bar
bak

foo
bar
Doing bonus
bak

Es gibt jetzt nur eine Version von MyFunc()mit dem boolParameter als Vorlagenargument.

Sebastian Stern
quelle
Fügt es nicht die Kompilierungszeit hinzu, indem Bonus () aufgerufen wird? Oder erkennt der Compiler, dass Bonus <false> leer ist und führt den Funktionsaufruf nicht aus?
Plougue
1
bonus<false>()Ruft die Standardversion der bonusVorlage auf (Zeilen 9 und 10 des Beispiels), sodass kein Funktionsaufruf erfolgt. Anders ausgedrückt: MyFunc()Kompiliert in einen Codeblock (ohne Bedingungen) und MyFunc<true>()in einen anderen Codeblock (ohne Bedingungen).
David K
6
@ plougue-Vorlagen sind implizit inline, und inline leere Funktionen bewirken nichts und können vom Compiler entfernt werden.
Yakk - Adam Nevraumont
8

Sie können Tag-Dispatching und einfache Funktionsüberladung verwenden:

struct Tag_EnableBonus {};
struct Tag_DisableBonus {};

void doBonusStuff(Tag_DisableBonus) {}

void doBonusStuff(Tag_EnableBonus)
{
    //Do bonus stuff here
}

template<class Tag> MyFunction(Tag bonus_tag)
{
   foo();
   bar();
   doBonusStuff(bonus_tag);
   foobar();
}

Dies ist leicht zu lesen / zu verstehen, kann ohne Schweiß (und ohne Heizplatte) erweitert werden if Klauseln - durch Hinzufügen weiterer Tags) erweitert werden und hinterlässt natürlich keinen Laufzeit-Footprint.

Die aufrufende Syntax ist zwar recht freundlich, kann aber natürlich in Vanille-Aufrufe eingepackt werden:

void MyFunctionAlone() { MyFunction(Tag_DisableBonus{}); }
void MyFunctionBonus() { MyFunction(Tag_EnableBonus{}); }

Das Versenden von Tags ist eine weit verbreitete generische Programmiertechnik. Hier ist ein schöner Beitrag über die Grundlagen.

Ap31
quelle