web-dev-qa-db-ja.com

std :: variantで参照が禁止されているのはなぜですか?

私はboost::variantをたくさん使っており、それに慣れています。 boost::variantは、バインドされた型を決して制限しません。特に、これらは参照である可能性があります。

#include <boost/variant.hpp>
#include <cassert>
int main() {
  int x = 3;
  boost::variant<int&, char&> v(x); // v can hold references
  boost::get<int>(v) = 4; // manipulate x through v
  assert(x == 4);
}

他のデータのビューとして参照のバリアントを使用するための実際のユースケースがあります。

するとstd::variantは制限付きの型としての参照を許可せず、std::variant<int&, char&>はコンパイルされず、 here と明示的に示されていることを知って驚いた。

バリアントは、参照、配列、または型voidを保持することはできません。

なぜこれが許されないのか、技術的な理由はわかりません。私はstd::variantboost::variantの実装が異なることを知っているので、おそらくそれと関係があるのでしょうか?それとも、著者はそれを安全ではないと考えましたか?

PS:参照ラッパーは基本型からの割り当てを許可しないため、std::variantを使用してstd::reference_wrapperの制限を実際に回避することはできません。

#include <variant>
#include <cassert>
#include <functional>

int main() {
  using int_ref = std::reference_wrapper<int>;
  int x = 3;
  std::variant<int_ref> v(std::ref(x)); // v can hold references
  static_cast<int&>(std::get<int_ref>(v)) = 4; // manipulate x through v, extra cast needed
  assert(x == 4);
}
18
olq_plo

基本的に、optionalvariantが参照型を許可しない理由は、そのような場合にどのような割り当て(およびより少ない程度で比較)を行うべきかについて意見の相違があるためです。 optionalは、variantよりも簡単に例に表示できるので、次のようにします。

int i = 4, j = 5;
std::optional<int&> o = i;
o = j; // (*)

マークされた行は、次のいずれかに解釈できます。

  1. &*o == &jなどのoを再バインドします。この行の結果として、iおよびjの値自体は変更されたままになります。
  2. oを介して割り当てます。このような&*o == &iはまだtrueですが、現在はi == 5です。
  3. 割り当てを完全に禁止します。

割り当てスルーは、=T=にプッシュするだけで得られる動作です。再バインドはより適切な実装であり、実際に必要なものです( この質問 も参照)。 参照タイプ )に関するMatt Calabreseの講演も同様です。

(1)と(2)の違いを説明する別の方法は、両方を外部で実装する方法です。

// rebind
o.emplace(j);

// assign through
if (o) {
    *o = j;
} else {
    o.emplace(j);
}

Boost.Optional ドキュメンテーションはこの根拠を提供します:

initializedオプション参照の割り当ての再バインドセマンティクスは、初期化状態間で一貫性を提供するために選択されました裸のC++参照のセマンティクスとの一貫性の欠如の費用。 optional<U>が初期化されるときは常に、Uと同じようにできるだけ多くの振る舞いをするように努めていることは事実です。ただし、UT&の場合、そうすることで、左辺値の初期化状態との動作に一貫性がなくなります。

参照オブジェクトへのoptional<T&>転送割り当て(したがって、参照オブジェクトの値を変更するが再バインドしない)を想像して、次のコードを検討してください。

optional<int&> a = get();
int x = 1 ;
int& rx = x ;
optional<int&> b(rx);
a = b ;

割り当ては何をしますか?

auninitializedである場合、その答えは明確です:xにバインドします(xへの別の参照があります)。しかし、aがすでに初期化されている場合はどうなりますか?参照されるオブジェクトの値を変更します(それが何であれ)。これは他の可能なケースと矛盾しています。

optional<T&>T&と同じように割り当てる場合、コードが割り当て後に機能するか、aが同じオブジェクトをエイリアスするかどうかにかかわらず、前の初期化状態を明示的に処理しないと、オプションの割り当てを使用できません。 bかどうか。

つまり、一貫性を保つために差別する必要があります。

コード内で別のオブジェクトへの再バインドがオプションではない場合、最初のバインドもそうではない可能性が非常に高いです。そのような場合、uninitializedoptional<T&>への代入は禁止されます。そのようなシナリオでは、左辺値がすでに初期化されている必要があることが前提条件である可能性がかなりあります。そうでない場合、最初のバインドは問題ありませんが、再バインドはそうではありません。このようなシナリオでは、次のように値自体を直接割り当てることができます。

assert(!!opt);
*opt=value;

その行が何をすべきかについての合意の欠如は、参照を完全に拒否するほうが簡単だったので、optionalvariantのほとんどの値は、少なくともC++ 17でそれを有効にして使い始めることができます。参照は常に後で追加することができます-またはそう議論は行きました。

16
Barry