2つのゼロ引数コンストラクターを区別する慣用的な方法


41

私はこのようなクラスを持っています:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

通常私countsは示されているようにデフォルト(ゼロ)配列を初期化したいと思います。

ただし、プロファイリングによって特定された特定の場所では、配列が上書きされることがわかっているため、配列の初期化を抑制したいのですが、コンパイラーはそれを理解するのに十分なほどスマートではありません。

そのような「セカンダリ」ゼロ引数コンストラクタを作成する慣用的で効率的な方法は何ですか?

現在、次のuninit_tagように、ダミー引数として渡されるタグクラスを使用しています。

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

次に、構築event_counts c(uninit_tag{});を抑制したいときのように、no-initコンストラクターを呼び出します。

私は、ダミークラスの作成を含まない、または何らかの方法でより効率的なソリューションなどを受け入れています。


「配列が上書きされることを知っているから」コンパイラーがその最適化をすでに行っていないことを100%確信していますか?適切な
フランク

6
@フランク-あなたの質問への答えはあなたが引用した文の後半にあるように思いますか?これは問題には含まれていませんが、さまざまなことが発生する可能性があります:(a)多くの場合、コンパイラーはデッドストアを排除するのに十分なほど強力ではありません(b)エレメントのサブセットのみが上書きされることがあり、これにより、最適化(ただし、同じサブセットのみが後で読み取られる)(c)コンパイラー実行できることもありますが、メソッドがインライン化されていないなどの理由で無効になります。
BeeOnRope

クラスに他のコンストラクタがありますか?
NathanOliver

1
@フランク-ええ、あなたの適例はgccが死んだ店を排除しないことを示していますか?実際、あなたが私に推測させたとしたら、gccがこの非常に単純なケースを正しく処理すると考えたでしょうが、ここで失敗した場合は、少し複雑なケースを想像してください!
BeeOnRope

1
@uneven_mark-はい、gcc 9.2は-O3でこれを実行します(ただし、この最適化は-O2、IMEに比べて一般的ではありません)が、以前のバージョンではそうではありませんでした。一般に、デッドストアの削除は重要ですが、非常に壊れやすく、コンパイラがデッドストアを確認すると同時に、支配的なストアを確認できるなど、通常のすべての注意事項の影響を受けます。私のコメントは、フランクが「ケースインポイント:(ゴッドボルトリンク)」と言ったために何を言おうとしているかを明確にするためのものでしたが、リンクは両方のストアが実行されていることを示しています(たぶん私は何かを見逃しています)。
BeeOnRope

回答:


33

あなたが既に持っている解決策は正しいです、そして私があなたのコードをレビューしているなら私が見たいと思うものと全く同じです。それは可能な限り効率的で、明確で簡潔です。


1
私の主な問題は、uninit_tagこのイディオムを使用したいすべての場所で新しいフレーバーを宣言する必要があるかどうかです。おそらく、おそらくそのようなインジケータータイプのようなものがすでにあると思いましたstd::
BeeOnRope

9
標準ライブラリからの明確な選択はありません。この機能が必要なすべてのクラスに新しいタグを定義するのではなく、プロジェクト全体のno_initタグを定義して、必要なすべてのクラスで使用します。
John Zwinck

2
標準ライブラリには、イテレータなどを区別するための男らしいタグと、2つとがあるstd::piecewise_construct_tと思いますstd::in_place_t。それらのどれもここで使用するのが合理的ではないようです。常に使用するタイプのグローバルオブジェクトを定義したいので、すべてのコンストラクター呼び出しで中括弧が必要ない場合があります。STLはstd::piecewise_constructforでこれを行いstd::piecewise_construct_tます。
n314159

それは可能な限り効率的ではありません。たとえばAArch64呼び出し規約では、タグはスタック割り当てで、ノックオン効果(どちらも末尾呼び出しできません...)を使用する必要があります:godbolt.org/z/6mSsmq
TLW

1
@TLWボディをコンストラクタに追加すると、スタック割り当てはありません。godbolt.org
z /

8

コンストラクター本体が空の場合、省略またはデフォルトにすることができます。

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

その後、デフォルトの初期化 event_counts counts;初期化されcounts.countsないままになり(デフォルトの初期化はここでは何もしません)、値の初期化 event_counts counts{};は値initializeでありcounts.counts、実質的にゼロで埋められます。


3
ただし、値の初期化を使用することを忘れないでください。OPはデフォルトで値の安全性を求めています。
文書

@doc、同意する。これは、OPが必要とする正確なソリューションではありません。ただし、この初期化は組み込み型を模倣しています。int i;我々はそれがゼロ初期化されていないことを受け入れます。また、event_counts counts;ゼロで初期化されていないものを受け入れevent_counts counts{};、新しいデフォルトにする必要があるかもしれません。
Evg

6

私はあなたの解決策が好きです。ネストされた構造体と静的変数も検討した可能性があります。例えば:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

静的変数を使用すると、初期化されていないコンストラクター呼び出しがより便利に思えるかもしれません。

event_counts e(event_counts::uninit);

もちろん、マクロを導入して入力を節約し、より体系的な機能にすることができます

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

3

enumはタグクラスやブール値よりも良い選択だと思います。構造体のインスタンスを渡す必要はなく、どのオプションを取得しているかは呼び出し元から明らかです。

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

次に、インスタンスの作成は次のようになります。

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

または、タグクラスのアプローチに近づけるには、タグクラスの代わりに単一値の列挙を使用します。

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

次に、インスタンスを作成する方法は2つしかありません。

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

私はあなたに同意します:enumの方が簡単です。しかし、おそらくあなたはこの行を忘れていました:event_counts() : counts{} {}
青みがかった

@青みがかった、私の意図はcounts無条件に初期化することではなく、がINIT設定されている場合のみでした。
TimK

@bluishタグクラスを選択する主な理由は、単純さを実現するためではなく、初期化されていないオブジェクトが特別であることを通知するためです。つまり、クラスインターフェイスの通常の部分ではなく最適化機能を使用します。boolenumはどちらもまともですが、オーバーロードの代わりにパラメーターを使用すると、意味の色合いが多少異なることに注意する必要があります。前者では、オブジェクトを明確にパラメーター化しているため、初期化済み/未初期化のスタンスがその状態になりますが、タグオブジェクトをctorに渡すのは、クラスに変換を実行するように要求するのと似ています。したがって、IMOは構文上の選択の問題ではありません。
文書

@TimKしかし、OPはデフォルトの動作が配列の初期化であることを望んでいるので、この問題の解決策にはを含める必要があると思いますevent_counts() : counts{} {}
青みがかった

@bluish私の最初の提案でcountsは、要求されstd::fillない限りによって初期化されNO_INITます。提案どおりにデフォルトのコンストラクターを追加すると、デフォルトの初期化を行う方法が2つありますが、これは素晴らしいアイデアではありません。の使用を回避する別のアプローチを追加しましたstd::fill
TimK

1

クラスの2フェーズの初期化を検討することをお勧めします。

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

上記のコンストラクターは、配列をゼロに初期化しません。配列の要素をゼロに設定するには、set_zero()作成後にメンバー関数を呼び出す必要があります。


7
おかげで、私はこのアプローチを検討しましたが、デフォルトを安全に保つ必要があります。つまり、デフォルトでゼロであり、選択されたいくつかの場所でのみ、安全でないものに動作をオーバーライドします。
BeeOnRope

3
これには、初期化されていないはずの使用方法を除いて特に注意が必要です。したがって、OPsソリューションに関連するバグの追加の原因となります。
ウォールナット

@BeeOnRopeは、デフォルトの引数std::functionと同様のコンストラクタ引数として提供することもできset_zeroます。初期化されていない配列が必要な場合は、ラムダ関数を渡します。
文書

1

私はそれをこのようにします:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

コンパイラーは、を使用するときにすべてのコードをスキップするのに十分賢くevent_counts(false)、クラスのインターフェースを奇妙なものにする代わりに、意味を正確に言うことができます。


8
効率は正しいですが、ブールパラメータはクライアントコードを読みやすくしません。一緒に読んでいて、宣言が表示されている場合event_counts(false)、それはどういう意味ですか?戻ってパラメータの名前を調べないと、何もわかりません。質問に示されているように、少なくとも列挙型、またはこの場合はセンチネル/タグクラスを使用することをお勧めします。次に、のような宣言を取得しますevent_counts(no_init)。これは、その意味で誰にとっても明らかです。
コーディグレイ

これもまともな解決策だと思います。デフォルトのctorを破棄して、デフォルト値を使用できますevent_counts(bool initCountr = true)
文書

また、ctorは明示的である必要があります。
文書

残念ながら現在C ++は名前付きパラメーターをサポートしていませんが、読みやすさboost::parameterを求めevent_counts(initCounts = false)て使用することができます
phuclv

1
おかしなことに、@ doc すべてのパラメーターがデフォルト値を持っているため、event_counts(bool initCounts = true)実際にデフォルトのコンストラクターです。要件は、引数を指定せずに呼び出し可能であること、event_counts ec;パラメーターなしであるかデフォルト値を使用するかを気にしないことです。
ジャスティンタイム-モニカを

1

入力を少し節約するためだけにサブクラスを使用します。

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

あなたは初期化しないと、コンストラクタの引数変更することにより、ダミークラスを取り除くことができますboolint、それはもう、ニーモニックである必要はありませんので、または何かを。

また、継承を交換events_count_no_initして、回答で提案されているEvgのようなデフォルトのコンストラクターで定義しevents_count、サブクラスにすることもできます。

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

これは面白いアイデアですが、新しいタイプを導入すると摩擦が発生するような気もします。たとえば、実際に初期化されていないevent_countsが必要な場合event_countevent_count_uninitialized、ではなくタイプにしたいので、のような構造で正しくスライスする必要event_counts c = event_counts_no_init{};があります。
BeeOnRope

@BeeOnRopeまあ、ほとんどの目的のためにevent_count_uninitializedオブジェクトはevent_countオブジェクトです。それが継承のすべてのポイントであり、それらは完全に異なるタイプではありません。
ロス・リッジ

同意するが、摩擦は「ほとんどの目的」である。それらは交換可能ではありません-たとえば、割り当てが機能ecuするecことを確認しようとした場合、その逆はできません。または、テンプレート関数を使用する場合、それらは異なる型であり、動作が同じになる場合でも、異なるインスタンス化で終了します(静的テンプレートメンバーなどとは異なる場合があります)。特にこれを頻繁に使用するとauto、間違いなく混乱して混乱する可能性があります。オブジェクトの初期化方法がその型に永続的に反映されるようにしたくありません。
BeeOnRope
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.