Дублікат коду за допомогою c ++ 11


80

Зараз я працюю над проектом, і у мене є наступне питання.

У мене є метод C ++, над яким я хочу працювати двома різними способами:

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

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

І я хотів би не дублювати мій код, оскільки фактична функція набагато довша. Проблема полягає в тому, що я ні за яких обставин не повинен додавати час виконання програми, коли MyFunction викликається замість MyFunctionWithABonus. Ось чому я не можу просто мати логічний параметр, який я перевіряю за допомогою порівняння C ++.

Моя ідея полягала б у використанні шаблонів C ++ для практично дублювання мого коду, але я не можу придумати спосіб, коли б у мене не було додаткового часу на виконання, і мені не потрібно дублювати код.

Я не фахівець із шаблонами, тому, можливо, чогось мені не вистачає.

Хтось із вас має ідею? Або це просто неможливо в C ++ 11?


64
Чи можу я запитати, чому ви не можете просто додати логічну перевірку? Якщо там багато коду, накладні витрати на просту булеву перевірку будуть незначними.
Джоріс,

39
Прогнозування @plougue Branch на сьогоднішній день дуже добре, до того, що на логічну перевірку часто потрібно 0 процесорних циклів.
Ден,

4
Погодьтеся з @Dan. Прогнозування філій в наші дні займає майже нульові витрати, особливо якщо ви входите до певної гілки велику кількість разів.
Акшай Арора

6
@Dan: Порівняння та розгалуження - це все-таки в кращому випадку один макрозлитий uop (на сучасних процесорах Intel і AMD x86 ), а не нуль. Залежно від того, яке вузьке місце є у вашому коді, декодування / видача / виконання цього uop може вкрасти цикл у чогось іншого, як і додаткова інструкція ADD. Крім того, просто передача логічного параметра та зв’язування реєстру (або його потрібно розлити / перезавантажити) є ненульовою кількістю інструкцій. Сподіваємось, ця функція вбудовується, тому накладні витрати на передачу дзвінків та передачі аргументів не є щоразу, і, можливо, cmp + гілка, але все ж
Пітер Кордес

15
Ви спочатку написали код у зручному для обслуговування форматі? Тоді ваш профіліст сказав, що гілка є вузьким місцем? Чи є у вас дані, які пропонують час, який ви витрачаєте на це незначне рішення, як найкраще використовувати свій час?
GManNickG

Відповіді:


55

За допомогою шаблону та лямбди ви можете зробити:

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

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

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

або ж ви можете просто створювати prefixта suffixфункціонувати.

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

void suffix()
{
    foobar();
}

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

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}

12
Я насправді віддаю перевагу цим двом рішенням над логічним параметром (шаблоном чи іншим способом), незалежно від будь-яких переваг часу виконання. Мені не подобаються логічні параметри.
Кріс Дрю,

2
З мого розуміння, друге рішення матиме додатковий час роботи через додатковий виклик функції. Чи це стосується першого? Я не впевнений, як лямбди працюють у такому випадку
плоуга

10
Якщо визначення видно, компілятор, ймовірно, вбудує код і згенерує той самий код, що і той, що згенерований для вашого вихідного коду.
Jarod42

1
@Yakk Я думаю, це буде залежати від конкретного випадку використання та від відповідальності "бонусних речей". Часто я виявляю, що серед основного алгоритму є параметри bool, ifs та бонусні речі, які ускладнюють читання, і я вважаю за краще "більше не існувати", а інкапсулювати та вводити з іншого місця. Але я думаю, що питання, коли доречно використовувати шаблон стратегії, можливо, виходить за рамки цього питання.
Кріс Дрю,

2
Оптимізація хвостового виклику зазвичай має значення, коли ви хочете оптимізувати рекурсивні випадки. У цьому випадку просте вкладання ... робить все, що вам потрібно.
Якк - Адам Неврамонт

128

Щось подібне буде робити добре:

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

Зателефонуйте через:

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

Шаблону "потворного" можна уникнути, додавши до функцій кілька приємних обгортків:

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

Ви можете знайти деяку цікаву інформацію на цій техніці там . Це «стара» папір, але сама по собі техніка залишається абсолютно правильною.

За умови, що у вас є доступ до гарного компілятора C ++ 17, ви навіть можете продовжити техніку, використовуючи constexpr, якщо :

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

11
І цю перевірку приємно оптимізуватиме компілятор
Йонас,

22
А в C ++ 17 if constexpr (bonus) { doBonusStuff(); } .
Кріс Дрю,

5
@ChrisDrew Не впевнений, що constexpr додасть що-небудь сюди. Було б?
Гібет,

13
@Gibet: Якщо дзвінок до doBonusStuff()навіть не може скомпілюватись з якихось причин у випадку без бонусів, це матиме величезну різницю.
Гонки легкості на орбіті

4
@WorldSEnder Так, ви могли б, якщо під переліченнями або класом перечислення ви маєте на увазі constexpr (бонус == MyBonus :: ExtraSpeed).
Гібет,

27

Беручи до уваги деякі коментарі, які OP зробив щодо налагодження, ось версія, яка вимагає doBonusStuff()збірки налагодження, але не випускає збірки (які визначають NDEBUG):

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

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

Ви також можете використовувати assertмакрос, якщо ви хочете перевірити умову, і не вдасться, якщо вона хибна (але тільки для збірок; збірки випуску перевірку не виконають).

Будьте обережні, якщо у вас doBonusStuff()є побічні ефекти, оскільки ці побічні ефекти не будуть присутні у збірках випусків і можуть призвести до втрати припущень, зроблених у коді.


Попередження про побічні ефекти є добрим, але воно також відповідає дійсності, незалежно від того, яка конструкція використовується, будь то шаблони, if () {...}, constexpr тощо
труба

Враховуючи коментарі ОП, я сам підтримав це, оскільки це саме найкраще рішення для них. Тим не менше, просто цікавість: чому всі ускладнення з новими визначає і все, коли ви можете просто помістити виклик doBonusStuff () всередині #if визначено (NDEBUG) ??
motoDrizzt

@motoDrizzt: Якщо ОП хоче зробити те саме в інших функціях, мені здається, що я ввожу новий макрос, такий як цей засіб для очищення / легший для читання (і запису). Якщо це лише одноразова річ, то я погоджуюсь, що використання #if defined(NDEBUG)безпосередньо, напевно, простіше.
Cornstalks

@Cornstalks так, це абсолютно має сенс, я не думав про це так далеко. І я все ще думаю, що це має бути прийнятою відповіддю :-)
motoDrizzt

18

Ось невелика варіація відповіді Jarod42 із використанням різних шаблонів, щоб абонент міг надати нуль або одну бонусну функцію:

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

Телефонний код:

MyFunction();
MyFunction(&doBonusStuff);

11

Інша версія, що використовує лише шаблони і не має перенаправляючих функцій, оскільки ви сказали, що не хочете зайвих витрат на виконання. Наскільки це швидко, це лише збільшує час компіляції:

#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

Зараз існує лише одна версія MyFunc()з boolпараметром як аргументом шаблону.


Хіба це не додає час компіляції, викликаючи bonus ()? Або компілятор виявляє, що bonus <false> порожній і не запускає виклик функції?
plougue

1
bonus<false>()викликає версію bonusшаблону за замовчуванням (рядки 9 та 10 прикладу), тому виклику функції немає. MyFunc()Іншими словами , компілюється в один блок коду (без умовних значень) і MyFunc<true>()компілюється в інший блок коду (без умовних значень).
Девід К

6
Шаблони @plougue неявно вбудовані, а вбудовані порожні функції нічого не роблять і можуть бути усунені компілятором.
Якк - Адам Неврамонт,

8

Ви можете використовувати диспетчеризацію тегів і просте перевантаження функції:

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

Це легко читати / розуміти, його можна розширити, не виділяючи поту (і не застосовуючи шаблонних ifположень - додаванням більшої кількості тегів), і, звичайно, не залишить сліду під час роботи.

Синтаксис виклику він досить зручний, але, звичайно, його можна обернути у ванільні дзвінки:

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

Диспетчеризація тегів - це широко використовувана загальна техніка програмування, ось приємний допис про основи.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.