web-dev-qa-db-ja.com

GCC9はstd :: variantの値のない状態を回避できますか?

私は最近、コンパイラ全体で_std::visit_最適化の素晴らしい比較につながるRedditディスカッションをフォローしました。私は次のことに気づきました: https://godbolt.org/z/D2Q5ED

GCC9とClang9の両方(同じstdlibを共有していると思います)は、すべての型がいくつかの条件を満たす場合に、値のない例外をチェックしてスローするためのコードを生成しません。これはより優れたcodegenにつながるため、MSVC STLで問題を提起し、次のコードが提示されました。

_template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}
_

主張は、これはすべてのバリアントを無価値にすることであり、 doc を読むと次のようになります:

まず、現在含まれている値(存在する場合)を破棄します。次に、_T_I_型の値を引数std::forward<Args>(args)....で構築するかのように、含まれている値を直接初期化します。例外がスローされると、_*this_はvalueless_by_exceptionになる場合があります。

わからないこと:なぜ「かもしれない」と記載されているのですか?操作全体がスローされた場合、古い状態を維持することは合法ですか?これはGCCが行うことなので、

_  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };
_

そして後でそれは(条件付きで)次のようなことをします:

_T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);
_

したがって、基本的には一時ファイルを作成し、それが成功すると、それを実際の場所にコピー/移動します。

IMOこれは、ドキュメントで述べられている「最初に、現在含まれている値を破棄する」の違反です。私が標準を読んだとき、v.emplace(...)の後、バリアントの現在の値は常に破棄され、新しい型はセット型または値なしのいずれかになります。

条件_is_trivially_copyable_は、監視可能なデストラクタを持つすべての型を除外します。したがって、これは、「as-ifバリアントが古い値で再初期化される」などと考えられます。しかし、バリアントの状態は観察可能な影響です。それで標準は実際に許可しますか、それはemplaceが現在の値を変更しないことですか?

標準的な見積もりに応じて編集します。

次に、含まれている値を、引数std​::​forward<Args>(args)...を使用してTI型の値を直接非リスト初期化するかのように初期化します。

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);は、上記の有効な実装として本当にカウントされますか?これは「まるで」の意味ですか?

14
Flamefire

標準の重要な部分はこれだと思います:

から https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4修飾子

(...)

テンプレートvariant_alternative_t>&emplace(Args && ... args);

(...)含まれている値の初期化中に例外がスローされた場合、バリアントは値を保持しない可能性があります

それは「する必要がある」ではなく「あるかもしれない」と言っていますこれは、gccで使用されるような実装を可能にするために意図的なものであると期待しています。

あなた自身が言ったように、これはすべての選択肢のデストラクタが取るに足らないものであり、以前の値を破壊する必要があるため観察できない場合にのみ可能です。

フォローアップの質問:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

T tmp {std :: forward(args)...}; this-> value = std :: move(tmp);上記の有効な実装として本当にカウントされますか?これは「まるで」の意味ですか?

はい。 簡単にコピーできる の型の場合、違いを検出する方法がないため、実装は、値が説明どおりに初期化されたかのように動作します。タイプが簡単にコピーできない場合、これは機能しません。

7
PaulR

それで標準は実際に許可しますか、それはemplaceが現在の値を変更しないことですか?

はい。 emplaceは、リークのないことの基本的な保証を提供します(つまり、構築と破壊が観察可能な副作用を生成するときにオブジェクトの寿命を尊重します)が、可能な場合、強力な保証を提供することが許可されます(つまり、元の状態が維持されます)操作が失敗したとき)。

variantはunionと同様に動作する必要があります—代替は適切に割り当てられたストレージの1つの領域に割り当てられます。動的メモリを割り当てることはできません。したがって、型を変更するemplaceは、追加の移動コンストラクターを呼び出さずに元のオブジェクトを保持する方法がありません。オブジェクトを破棄して、代わりに新しいオブジェクトを構築する必要があります。この構築が失敗した場合、バリアントは非常に価値のない状態に移行する必要があります。これにより、存在しないオブジェクトを破壊するような奇妙なことを防ぎます。

ただし、小さな自明なコピー可能な型の場合、オーバーヘッドをあまりかけずに強力な保証を提供することができます(この場合、チェックを回避するためのパフォーマンスが向上します)。したがって、実装はそれを行います。これは標準に準拠しています。実装は、標準により要求される基本的な保証を、よりユーザーフレンドリーな方法で提供します。

標準的な見積もりに応じて編集します。

次に、含まれている値を、引数std​::​forward<Args>(args)...を使用してTI型の値を直接非リスト初期化するかのように初期化します。

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);は、上記の有効な実装として本当にカウントされますか?これは「まるで」の意味ですか?

はい、移動割り当てが観察可能な効果をもたらさない場合、それは自明にコピー可能なタイプの場合です。

5
L. F.