【问题标题】:Duplicate code using c++11使用 c++11 复制代码
【发布时间】:2017-09-22 03:29:18
【问题描述】:

我目前正在做一个项目,但遇到以下问题。

我有一个 C++ 方法,我想以两种不同的方式工作:

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

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

而且我不想复制我的代码,因为实际的函数要长得多。 问题是我在任何情况下都不能在调用 MyFunction 而不是 MyFunctionWithABonus 时向程序添加执行时间。这就是为什么我不能只拥有一个通过 C++ 比较检查的布尔参数。

我的想法是使用 C++ 模板来虚拟复制我的代码,但我想不出没有额外执行时间且不必复制代码的方法。

我不是模板专家,所以我可能会遗漏一些东西。

你们有什么想法吗?或者这在 C++11 中是不可能的?

【问题讨论】:

  • 请问为什么你不能简单地添加一个布尔检查?如果里面有很多代码,那么简单的布尔检查的开销可以忽略不计。
  • @plougue 分支预测现在非常好,以至于布尔检查通常需要 0 个处理器周期来执行。
  • 同意@Dan。如今,分支预测的开销几乎为零,尤其是当您多次进入特定分支时。
  • @Dan:compare-and-branch 充其量仍然是一个宏融合的 uop(在现代 Intel 和 AMD x86 CPU 上),而不是零。根据代码中的瓶颈,解码/发出/执行此 uop 可能会从其他东西中窃取一个周期,就像额外的 ADD 指令一样。此外,仅传递布尔参数并让它占用一个寄存器(或必须溢出/重新加载)是非零数量的指令。希望这个函数内联,所以调用和参数传递的开销不是每次都存在,也许是 cmp+branch,但仍然
  • 你是不是先把代码写成易于维护的格式?那么你的分析器是否说分支是瓶颈?您是否有数据表明您花在这个小决定上的时间最好地利用了您的时间?

标签: c++ c++11 templates


【解决方案1】:

这样的事情会做得很好:

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

您可以在there 找到有关该技术的一些不错的信息。那是一篇“旧”论文,但技术本身完全正确。

如果您可以访问一个不错的 C++17 编译器,您甚至可以通过使用 constexpr if 进一步推动该技术,如下所示:

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

【讨论】:

  • 编译器会很好地优化该检查
  • 还有in C++17 if constexpr (bonus) { doBonusStuff(); }
  • @Gibet:如果对doBonusStuff() 的调用在非奖励情况下由于某种原因甚至无法编译,那将产生巨大的影响。
  • @Gibet 对于您的 C++17 示例,最好在 if constexpr 链的末尾添加 else { static_assert(false);}。这样它就不能用非预期的值模板化。
  • 我认为答案的第一部分可能会添加内联函数定义,为 MyFunction&lt;true&gt;MyFunction&lt;false&gt; 提供单独的名称,以便调用者无需提供显式模板参数,并且可以确实忘记了她正在调用一个函数模板。
【解决方案2】:

使用模板和 lambda,您可以这样做:

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

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

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

否则你可以只创建prefixsuffix 函数。

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

void suffix()
{
    foobar();
}

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

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

【讨论】:

  • 我实际上更喜欢这两种解决方案而不是布尔参数(模板或其他),无论执行时间优势如何。我不喜欢布尔参数。
  • 据我了解,由于额外的函数调用,第二个解决方案将有额外的运行时间。第一个是这种情况吗?我不确定 lambda 在这种情况下是如何工作的
  • 如果定义可见,编译器可能会内联代码并生成与为原始代码生成的代码相同的代码。
  • @Yakk 我认为这将取决于特定的用例以及“奖励的东西”是谁的责任。我经常发现在主要算法中包含 bool 参数、ifs 和奖励的东西会使它更难阅读,并且希望它“不再存在”并被封装并从其他地方注入。但我认为何时适合使用策略模式的问题可能超出了这个问题的范围。
  • 尾调用优化通常在您想要优化递归案例时很重要。在这种情况下,简单的内联......可以满足您的一切需求。
【解决方案3】:

鉴于 OP 在调试方面所做的一些 cmets,这里有一个版本调用 doBonusStuff() 进行调试构建,而不是发布构建(定义 NDEBUG):

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

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

如果您希望检查条件,也可以使用assert macro,如果条件为假则失败(但仅适用于调试版本;发布版本不会执行检查)。

如果doBonusStuff() 有副作用,请小心,因为这些副作用不会出现在发布版本中,并且可能会使代码中的假设无效。

【讨论】:

  • 关于副作用的警告是好的,但无论使用什么构造,无论是模板、if(){...}、constexpr 等,它也是如此。
  • 鉴于 OP cmets,我自己对此表示赞同,因为这正是他们的最佳解决方案。也就是说,只是好奇:为什么新定义的所有复杂性和一切,当您可以将 doBonusStuff() 调用放在 #if defined(NDEBUG) 中时??
  • @motoDrizzt:如果 OP 想在其他函数中做同样的事情,我发现引入了一个新的宏,比如这个更清洁/更容易阅读(和编写)的宏。如果这只是一次性的事情,那么我同意直接使用#if defined(NDEBUG) 可能更容易。
  • @Cornstalks 是的,这完全有道理,我没想到那么远。而且我仍然认为这应该是公认的答案:-)
【解决方案4】:

以下是 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);

【讨论】:

    【解决方案5】:

    另一个版本,只使用模板,没有重定向功能,因为你说你不想要任何运行时开销。就我而言,这只会增加编译时间:

    #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 为空并且不运行函数调用?
    • bonus&lt;false&gt;() 调用bonus 模板的默认版本(示例的第 9 和 10 行),因此没有函数调用。换句话说,MyFunc() 编译为一个代码块(其中没有条件),MyFunc&lt;true&gt;() 编译为另一个代码块(其中没有条件)。
    • @plougue 模板是隐式内联的,内联的空函数不做任何事情,可以被编译器消除。
    【解决方案6】:

    您可以使用标签调度和简单的函数重载:

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

    标签调度是一种广泛使用的通用编程技术,here 是一篇关于基础知识的好帖子。

    【讨论】:

      猜你喜欢
      • 2016-03-31
      • 1970-01-01
      • 1970-01-01
      • 2013-08-08
      • 1970-01-01
      • 1970-01-01
      • 2012-11-16
      • 2022-11-16
      • 1970-01-01
      相关资源
      最近更新 更多