c ++ 11を使用してコードを複製する


80

私は現在プロジェクトに取り組んでおり、次の問題があります。

私は2つの異なる方法で働きたいC ++メソッドを持っています:

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

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

実際の関数ははるかに長いので、コードを複製したくありません。問題は、MyFunctionWithABonusの代わりにMyFunctionが呼び出されたときに、いかなる状況でもプログラムに実行時間を追加してはならないということです。そのため、C ++の比較でチェックするブールパラメータだけを使用することはできません。

私の考えは、C ++テンプレートを使用してコードを仮想的に複製することでしたが、追加の実行時間がなく、コードを複製する必要がない方法を考えることはできません。

私はテンプレートの専門家ではないので、何かが足りないかもしれません。

誰かアイデアがありますか?それとも、C ++ 11ではそれは不可能ですか?


64
私が聞いても、なぜあなたは、単にブールチェックを追加することはできませんか?そこに多くのコードがある場合、単純なブールチェックのオーバーヘッドは無視できます。
Joris 2017

39
@plougue分岐予測は最近非常に優れており、ブールチェックの実行に0プロセッササイクルかかることがよくあります。
ダン

4
@Danに同意します。最近、特に特定のブランチに何度も入る場合、ブランチ予測のオーバーヘッドはほとんどありません。
Akshay Arora 2017

6
@Dan:比較と分岐は、ゼロではなく、せいぜい1つのマクロ融合uop(最新のIntelおよびAMD x86 CPU)です。コードのボトルネックによっては、このuopをデコード/発行/実行すると、追加のADD命令と同じように、他の何かからサイクルが盗まれる可能性があります。また、ブールパラメータを渡すだけで、レジスタを拘束する(またはスピル/リロードする必要がある)ことは、ゼロ以外の数の命令です。うまくいけば、この関数がインライン化されるので、呼び出しと引数の受け渡しのオーバーヘッドが毎回存在するわけではなく、おそらくcmp + branchですが、それでも
Peter Cordes 2017

15
最初に、保守しやすい形式でコードを記述しましたか?次に、プロファイラーはブランチがボトルネックであると言いましたか?このマイナーな決定に費やしている時間を、自分の時間を最大限に活用することを示唆するデータはありますか?
GManNickG 2017

回答:


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つのソリューションの方が好きです。ブールパラメータが嫌いです。
クリス・ドリュー

2
私の理解では、2番目のソリューションには、追加の関数呼び出しのために追加のランタイムがあります。これは最初のものの場合ですか?その場合、ラムダがどのように機能するかわかりません
plougue 2017

10
定義が表示されている場合、コンパイラはおそらくコードをインライン化し、元のコード用に生成されたものと同じコードを生成します。
jarod42 2017

1
@Yakkそれは特定のユースケースとその責任が「ボーナス」であるかどうかに依存すると思います。多くの場合、メインアルゴリズムの中にブールパラメータ、if、ボーナスが含まれていると、読みにくくなり、「存在しなくなった」状態でカプセル化され、他の場所から注入されることを好みます。しかし、戦略パターンをいつ使用するのが適切かという問題は、おそらくこの質問の範囲を超えていると思います。
クリス・ドリュー

2
再帰的なケースを最適化する場合は、通常、末尾呼び出しの最適化が重要になります。この場合、単純なインライン化...必要なすべてを実行します。
Yakk-Adam Nevraumont 2017

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

11
そして、そのチェックはコンパイラーによってうまく最適化されます
Jonas

22
そしてC ++ 17では if constexpr (bonus) { doBonusStuff(); }
クリス・ドリュー

5
@ChrisDrewconstexprがここに何かを追加するかどうかはわかりません。それでしょうか?
絞首台2017

13
@Gibet:doBonusStuff()ボーナス以外の場合、何らかの理由でへの呼び出しをコンパイルすることさえできない場合、それは大きな違いを生むでしょう。
軌道上でのライトネスレース2017年

4
@WorldSEnderはい、できます。列挙型または列挙型クラスでconstexpr(bonus == MyBonus :: ExtraSpeed)を意味する場合。
絞首台2017

27

OPがデバッグに関して行ったコメントのいくつかを考慮して、doBonusStuff()デバッグビルドを要求するが、リリースビルド(を定義するNDEBUG)を要求しないバージョンを次に示します。

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

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

条件をチェックしたい場合はassertマクロを使用し、それがfalseの場合は失敗することもできます(ただし、デバッグビルドの場合のみ。リリースビルドはチェックを実行しません)。

doBonusStuff()副作用がある場合は注意してください。これらの副作用はリリースビルドには存在せず、コードで行われた仮定が無効になる可能性があります。


副作用に関する警告は適切ですが、テンプレート、if(){...}、constexprなど、どの構成が使用されていても当てはまります。–
パイプ

OPのコメントを踏まえて、私はこれを自分で賛成しました。これがまさに彼らにとって最良の解決策だからです。とはいえ、好奇心だけです。doBonusStuff()呼び出しを#if defined(NDEBUG)内に置くことができるのに、なぜ新しい定義とすべてのすべての複雑さがあるのでしょうか。
motoDrizzt 2017

@motoDrizzt:OPが他の関数でこれと同じことをしたい場合は、このクリーナー/読み取り(および書き込み)が簡単な新しいマクロを導入します。それが1回限りのことであれば、#if defined(NDEBUG)直接使用する方がおそらく簡単であることに同意します。
Cornstalks 2017

@Cornstalksうん、それは完全に理にかなっている、私はそれについてそれについて考えていなかった。そして、私はまだこれが受け入れられた答えであるべきだと思っています:-)
motoDrizzt 2017

18

発信者がゼロまたは1つのボーナス関数を提供できるように、可変個引数テンプレートを使用した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()持つのバージョンは1つだけboolです。


Bonus()を呼び出すことでコンパイル時間を追加しませんか?または、コンパイラは、bonus <false>が空であり、関数呼び出しを実行しないことを検出しますか?
plougue 2017

1
bonus<false>()bonusテンプレートのデフォルトバージョン(例の9行目と10行目)を呼び出すため、関数呼び出しはありません。別の言い方をすれば、MyFunc()1つのコードブロック(条件付きなし)にMyFunc<true>()コンパイルし、別のコードブロック(条件付きなし)にコンパイルします。
David K

6
@plougueテンプレートは暗黙的にインラインであり、インライン化された空の関数は何も実行せず、コンパイラーによって削除できます。
Yakk-Adam Nevraumont 2017

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

タグディスパッチは、広く使用されているジェネリックプログラミング手法です。ここに、基本についてのすばらしい投稿があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.