1つの関数呼び出しC ++を使用して複数の定数クラスメンバーを初期化する


50

2つの異なる定数メンバー変数があり、どちらも同じ関数呼び出しに基づいて初期化する必要がある場合、関数を2回呼び出さずにこれを行う方法はありますか?

たとえば、分子と分母が定数である分数クラス。

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

これにより、GCD関数が2回呼び出されるため、時間が無駄になります。新しいクラスメンバーを定義し、gcd_a_b最初にgcdの出力を初期化リストのそれに割り当てることもできますが、これによりメモリが無駄になります。

一般に、関数呼び出しやメモリを無駄にせずにこれを行う方法はありますか?初期化リストに一時変数を作成できますか?ありがとうございました。


5
「GCD関数が2回呼び出されている」という証拠はありますか?これは2度言及されていますが、2度呼び出すコードを生成するコンパイラーと同じではありません。コンパイラは、それが純粋な関数であると推定し、2番目の言及でその値を再利用します。
エリックタワーズ

6
@EricTowers:はい、場合によっては、コンパイラーが実際に問題を回避できることがあります。ただし、定義(またはオブジェクト内の注釈)を見ることができる場合のみ、そうでない場合は純粋であることを証明できません。リンク時の最適化を有効にしてコンパイルする必要がありますが、誰もがそうするわけではありません。そして関数はライブラリにあるかもしれません。または関数の場合を検討、副作用を持っており、一度だけ、それを呼び出すと正しさの問題ですか?
Peter Cordes

@EricTowers興味深い点。実際に、printステートメントをGCD関数内に置くことで確認しようとしましたが、これでは純粋な関数になるのを妨げることに気づきました。
Qq0

@ Qq0:たとえば、gccまたはclangでGodboltコンパイラーエクスプローラーを使用して、コンパイラーが生成したasmを調べることで確認でき-O3ます。しかし、おそらく任意の単純なテスト実装では、実際には関数呼び出しがインライン化されます。__attribute__((const))可視的な定義を提供せずにプロトタイプを使用するか純粋な場合、GCCまたはclangは、同じ引数を持つ2つの呼び出し間で共通部分式の除去(CSE)を実行できます。Drewの回答は純粋でない関数でも機能するため、はるかに優れており、funcがインライン化しない可能性がある場合はいつでも使用する必要があります。
Peter Cordes

一般に、非静的constメンバー変数は回避するのが最善です。constのすべてが適用されない数少ない領域の1つ。たとえば、クラスオブジェクトを割り当てることはできません。容量制限がサイズ変更に影響しない限り、ベクトルにemplace_backを実行できます。
ダグ

回答:


66

一般に、関数呼び出しやメモリを無駄にせずにこれを行う方法はありますか?

はい。これは、C ++ 11で導入された委任コンストラクターを使用して行うことができます。

委任コンストラクタは前に建設するために必要な一時的な値を取得するための非常に効率的な方法である任意のメンバ変数が初期化されています。

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};

興味深いことに、別のコンストラクターを呼び出すことによるオーバーヘッドは重要ですか?
Qq0

1
@ Qq0 適度な最適化を有効にしてもオーバーヘッドがないことをここ確認できます。
Drew Dormann

2
@ Qq0:C ++は、最新の最適化コンパイラを中心に設計されています。.h実際のコンストラクター定義がインライン化のために表示されない場合でも、特にクラス定義(で)で可視化すると、このデリゲーションを簡単にインライン化できます。つまり、gcd()呼び出しは各コンストラクターの呼び出しサイトにインライン化callし、3オペランドのプライベートコンストラクターだけに任せます。
Peter Cordes

10

メンバー変数は、クラスdeclerationで宣言された順序で初期化されるため、次のことができます(数学的に)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

別のコンストラクタを呼び出したり、作成する必要さえありません。


6
これは特にGCDで機能しますが、他の多くのユースケースではおそらく、argsと最初のconstを2番目のconstから導出できません。そして、書かれているように、これにはコンパイラーが最適化しない可能性がある理想的とは別のマイナス面である1つの余分な除算があります。GCDのコストは1部門だけなので、GCDを2回呼び出すのと同じくらい悪い結果になる可能性があります。(最近のCPUでよく行われるように、除算が他の操作のコストを支配すると仮定します。)
Peter Cordes

@PeterCordesですが、他のソリューションには追加の関数呼び出しがあり、より多くの命令メモリを割り当てます。
asmmo

1
ドリューの委任コンストラクタについて話していますか?これにより、Fraction(a,b,gcd(a,b))委任を呼び出し元にインライン化できるため、総コストを削減できます。そのインライン化は、コンパイラーが行う余分な除算を元に戻すよりも簡単です。godbolt.orgでは試しませんでしたが、好奇心旺盛な人ならできます。-O3通常のビルドと同じようにgccまたはclang を使用します。(C ++は、最新の最適化コンパイラを想定して設計されているため、などの機能constexpr
Peter Cordes

-3

@Drew Dormannは、私が考えていたものと同様のソリューションを提供しました。OPはctorを変更できないことについて決して言及しないので、これは次のように呼び出すことができますFraction f {a, b, gcd(a, b)}

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

この方法でのみ、関数、コンストラクターなどの2番目の呼び出しがないため、時間の無駄がありません。とにかく一時ファイルを作成する必要があるので、それは無駄なメモリではありませんので、それをうまく利用することもできます。また、余分な分割を回避します。


3
あなたの編集はそれが質問に答えさえしないようにします。次に、呼び出し元に3番目の引数を渡すように要求していますか?コンストラクタ本体内で代入を使用する元のバージョンはconst、では機能しませんが、少なくとも他のタイプでは機能します。そして、あなたも「また」あなたが避けている余分な分割は何ですか?あなたはvs. asmmoの答えを意味しますか?
Peter Cordes

1
さて、あなたはあなたのポイントを説明したので、今では私の反対投票を削除しました。しかし、これは明らかにひどいようで、すべての呼び出し元にコンストラクター作業の一部を手動でインライン化する必要があります。これは、DRY(自分を繰り返さないでください)およびクラスの責任/内部構造のカプセル化の反対です。ほとんどの人はこれを許容できる解決策とは考えていません。これをきれいに行うC ++ 11の方法があることを考えると、古いC ++バージョンでスタックしておらず、クラスがこのコンストラクターをほとんど呼び出さない限り、誰もこれを行うべきではありません。
Peter Cordes

2
@aconcernedcitizen:パフォーマンス上の理由ではなく、コード品質の理由によるものです。あなたのやり方で、このクラスが内部でどのように機能するかを変更した場合、コンストラクターへのすべての呼び出しを見つけて、その3番目の引数を変更する必要があります。その余分,gcd(foo, bar)なコードは、ソース内のすべての呼び出しサイトから除外することができるため、除外する必要がある追加のコードです。これは保守性/可読性の問題であり、パフォーマンスではありません。ほとんどの場合、コンパイラーはコンパイル時にインライン化しますが、これはパフォーマンスのために必要です。
Peter Cordes

1
@PeterCordesそうですね、今では解決策に心が決まっており、他のことはすべて無視しています。どちらにせよ、恥ずかしさだけでも答えは残ります。私はそれについて疑問があるときはいつでも、どこを探すべきかを知っています。
関係者

1
また、Fraction f( x+y, a+b ); 自分の方法で書き込むには、BadFraction f( x+y, a+b, gcd(x+y, a+b) );tmp vars を書き込むか使用する必要がある場合も考慮してください。またはさらに悪いことに、もしあなたが書こうとしたらどうなるでしょうかFraction f( foo(x), bar(y) );-戻り値を保持するためにいくつかのtmp変数を宣言するための呼び出しサイトが必要か、またはそれらの関数を再度呼び出してコンパイラがそれらをCSEで削除することを期待します。これは避けていることです。1つの呼び出し元が引数を混合するケースをデバッグして、gcd実際にコンストラクタに渡される最初の2つの引数のGCDではないようにしますか?番号?次に、そのバグを可能にしないでください。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.