Powiel kod przy użyciu języka C ++ 11

80

Obecnie pracuję nad projektem i mam następujący problem.

Mam metodę C ++, którą chcę pracować na dwa różne sposoby:

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

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

I nie chciałbym powielać swojego kodu, ponieważ rzeczywista funkcja jest znacznie dłuższa. Problem polega na tym, że nie mogę w żadnym wypadku dodawać czasu wykonania do programu, gdy wywoływana jest funkcja MyFunction zamiast MyFunctionWithABonus. Dlatego nie mogę po prostu mieć parametru boolowskiego, który sprawdzę za pomocą porównania C ++.

Moim pomysłem byłoby użycie szablonów C ++ do wirtualnego duplikowania mojego kodu, ale nie mogę wymyślić sposobu, w którym nie mam dodatkowego czasu na wykonanie i nie muszę duplikować kodu.

Nie jestem ekspertem od szablonów, więc być może czegoś mi brakuje.

Czy ktoś z Was ma pomysł? A może jest to po prostu niemożliwe w C ++ 11?

plougue
źródło
64
Czy mogę zapytać, dlaczego nie możesz po prostu dodać czeku logicznego? Jeśli jest tam dużo kodu, narzut prostego sprawdzenia boolowskiego będzie pomijalny.
Joris
39
@plougue Przewidywanie gałęzi jest obecnie bardzo dobre, do tego stopnia, że ​​wykonanie sprawdzenia logicznego często zajmuje 0 cykli procesora.
Dan
4
Zgadzam się z @Dan. Przewidywanie gałęzi zajmuje obecnie prawie zero narzutów, zwłaszcza jeśli wchodzisz do określonej gałęzi wiele razy.
Akshay Arora
6
@Dan: Porównanie i rozgałęzienie jest nadal w najlepszym przypadku jednym uop połączonym z makro (na nowoczesnych procesorach Intel i AMD x86 ), a nie zerem. W zależności od tego, jakie wąskie gardło jest w twoim kodzie, dekodowanie / wydanie / wykonanie tego uop może ukraść cykl z czegoś innego, tak samo jak dodatkowa instrukcja ADD. Ponadto samo przekazanie parametru boolowskiego i związanie go z rejestrem (lub konieczność rozlania / ponownego załadowania) to niezerowa liczba instrukcji. Miejmy nadzieję, że ta funkcja jest wbudowana, więc wywołanie i przepuszczanie argumentów nie są dostępne za każdym razem, a może cmp + branch, ale mimo to
Peter Cordes
15
Czy najpierw napisałeś kod w łatwym w utrzymaniu formacie? W takim razie twój profiler powiedział, że gałąź była wąskim gardłem? Czy masz dane, które sugerują, że czas poświęcony na tę drobną decyzję jest najlepszym sposobem wykorzystania czasu?
GManNickG

Odpowiedzi:

55

Za pomocą szablonu i lambdy możesz:

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

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

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

albo możesz po prostu tworzyć prefixi suffixfunkcjonować.

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

void suffix()
{
    foobar();
}

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

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}
Jarod42
źródło
12
Właściwie wolę te dwa rozwiązania niż parametr boolowski (szablon lub inny), niezależnie od jakichkolwiek korzyści związanych z czasem wykonania. Nie lubię parametrów boolowskich.
Chris Drew
2
Z mojego zrozumienia, drugie rozwiązanie będzie miało dodatkowe środowisko wykonawcze ze względu na dodatkowe wywołanie funkcji. Czy tak jest w przypadku pierwszego? Nie jestem pewien, jak działają
lambdy
10
Jeśli definicje są widoczne, kompilator prawdopodobnie wbudowałby kod i wygenerowałby taki sam kod, jak ten wygenerowany dla oryginalnego kodu.
Jarod42
1
@Yakk Myślę, że będzie to zależeć od konkretnego przypadku użycia i odpowiedzialności za „dodatkowe rzeczy”. Często znajduję parametry bool, ifs i dodatkowe rzeczy w głównym algorytmie, co utrudnia odczytanie i wolałbym, aby „już nie istniał” i został zamknięty i wstrzyknięty z innego miejsca. Ale myślę, że kwestia tego, kiedy należy zastosować wzorzec strategii, prawdopodobnie wykracza poza zakres tego pytania.
Chris Drew
2
Optymalizacja połączeń ogonowych zwykle ma znaczenie, gdy chcesz zoptymalizować przypadki rekurencyjne. W tym przypadku zwykłe podszycie ... robi wszystko, czego potrzebujesz.
Yakk - Adam Nevraumont
128

Coś takiego ładnie się nada:

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

Zadzwoń przez:

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

Wszystkich „brzydkich” szablonów można uniknąć, dodając do funkcji kilka ładnych opakowań:

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

Można znaleźć kilka ciekawych informacji na temat tej techniki tam . To jest „stary” artykuł, ale technika sama w sobie pozostaje całkowicie poprawna.

Pod warunkiem, że masz dostęp do ładnego kompilatora C ++ 17, możesz nawet rozwinąć tę technikę, używając constexpr if , w ten sposób:

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
źródło
11
I to sprawdzenie zostanie dobrze zoptymalizowane przez kompilator
Jonas
22
Oraz w C ++ 17 if constexpr (bonus) { doBonusStuff(); } .
Chris Drew
5
@ChrisDrew Nie jestem pewien, czy constexpr, czy to coś tutaj doda. Czy to by było?
Gibet
13
@Gibet: Jeśli wywołanie do doBonusStuff()nie może nawet się skompilować z jakiegoś powodu w przypadku bez premii, zrobi to ogromną różnicę.
Wyścigi lekkości na orbicie
4
@WorldSEnder Tak, możesz, jeśli przez wyliczenia lub klasę enum masz na myśli constexpr (bonus == MyBonus :: ExtraSpeed).
Gibet
27

Biorąc pod uwagę niektóre komentarze OP dotyczące debugowania, oto wersja, która wymaga doBonusStuff()kompilacji debugowania, ale nie wersji wydania (które definiują NDEBUG):

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

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

Możesz również użyć assertmakra, jeśli chcesz sprawdzić warunek i zakończyć się niepowodzeniem, jeśli jest fałszywy (ale tylko w przypadku kompilacji debugowania; kompilacje wydania nie przeprowadzą sprawdzenia).

Uważaj, jeśli doBonusStuff()ma efekty uboczne, ponieważ te efekty uboczne nie będą obecne w kompilacjach wydań i mogą unieważnić założenia przyjęte w kodzie.

Łodygi kukurydzy
źródło
Ostrzeżenie o skutkach ubocznych jest dobre, ale jest również prawdziwe bez względu na to, jaki konstrukt jest używany, czy to szablony, czy () {...}, constexpr, itp.
pipe
Biorąc pod uwagę komentarze OP, sam to zagłosowałem, ponieważ jest to dla nich najlepsze rozwiązanie. To powiedziawszy, tylko ciekawostka: po co wszystkie komplikacje związane z nowymi definicjami i wszystkim tym, skoro możesz po prostu umieścić wywołanie doBonusStuff () wewnątrz zdefiniowanego #if (NDEBUG)?
motoDrizzt
@motoDrizzt: Jeśli OP chce zrobić to samo w innych funkcjach, uważam, że wprowadzenie nowego makra, takiego jak to, jest bardziej przejrzyste / łatwiejsze do odczytania (i zapisu). Jeśli jest to jednorazowa rzecz, zgadzam się, że użycie #if defined(NDEBUG)bezpośrednio jest prawdopodobnie łatwiejsze.
Cornstalks
@Cornstalks tak, to ma sens, nie myślałem o tym tak daleko. I wciąż myślę, że to powinna być akceptowana odpowiedź :-)
motoDrizzt
18

Oto niewielka odmiana odpowiedzi Jarod42 przy użyciu szablonów wariadycznych, aby dzwoniący mógł zapewnić zero lub jedną funkcję bonusową:

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();
}

Kod telefoniczny:

MyFunction();
MyFunction(&doBonusStuff);
Chris Drew
źródło
11

Inna wersja, wykorzystująca tylko szablony i bez funkcji przekierowujących, ponieważ powiedziałeś, że nie chcesz żadnego obciążenia związanego z uruchomieniem. O ile mi wiadomo, wydłuża to tylko czas kompilacji:

#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

Jest teraz tylko jedna wersja MyFunc()z boolparametrem jako argumentem szablonu.

Sebastian Stern
źródło
Czy nie wydłuża czasu kompilacji, wywołując bonus ()? A może kompilator wykrył, że bonus <false> jest pusty i nie uruchamia wywołania funkcji?
plougue
1
bonus<false>()wywołuje domyślną wersję bonusszablonu (wiersze 9 i 10 przykładu), więc nie ma wywołania funkcji. Innymi słowy, MyFunc()kompiluje się do jednego bloku kodu (bez żadnych warunków) i MyFunc<true>()kompiluje do innego bloku kodu (bez żadnych warunków).
David K
6
Szablony @plougue są niejawnie wbudowane, a wbudowane puste funkcje nic nie robią i mogą zostać wyeliminowane przez kompilator.
Yakk - Adam Nevraumont
8

Możesz użyć wysyłania tagów i prostego przeciążenia funkcji:

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();
}

Jest to łatwe do odczytania / zrozumienia, można je bez wysiłku rozszerzyć (i bez standardowych ifklauzul - dodając więcej tagów) i oczywiście nie pozostawi śladu w czasie wykonywania.

Składnia wywołania jest całkiem przyjazna, ale oczywiście można ją zawinąć w wywołania waniliowe:

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

Wysyłanie tagów jest szeroko stosowaną ogólną techniką programowania. Oto fajny post o podstawach.

Ap31
źródło