【问题标题】:Why are references forbidden in std::variant?为什么在 std::variant 中禁止引用?
【发布时间】:2019-06-10 15:07:22
【问题描述】:

我经常使用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&lt;int&amp;, char&amp;&gt; 无法编译,它明确表示 here

不允许变量包含引用、数组或 void 类型。

我想知道为什么不允许这样做,我没有看到技术原因。我知道std::variantboost::variant 的实现是不同的,所以也许与此有关?还是作者认为它不安全?

PS:我无法真正解决使用std::reference_wrapperstd::variant 的限制,因为引用包装器不允许从基类型进行赋值。

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

【问题讨论】:

  • 一个演员或std::get&lt;int_ref&gt;(v).get(),公平地说。
  • 你可以使用指针吗?
  • optional&lt;T&amp;&gt; 相同的问题:您是通过引用分配还是重新安装引用?
  • @Galik 在我的应用程序中,我可以让它与指针一起工作,但我必须编写很多额外的代码。我有很多 boost::get 调用,如果变体包含完整类型或引用,它们可以无缝工作。它不能与指针无缝地工作。
  • 对不起,应该说boost::getboost::apply_visitor 电话。我可以简单地包装第一个来无缝处理指针,但是包装第二个并不容易。

标签: c++ boost c++17 variant


【解决方案1】:

从根本上说,optionalvariant 不允许引用类型的原因是对于这种情况下应该做什么分配(以及在较小程度上是比较)存在分歧。 optionalvariant 更容易在示例中显示,所以我会坚持下去:

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

标记的行可以解释为:

  1. 重新绑定o,这样&amp;*o == &amp;j。由于这一行,ij 的值本身保持不变。
  2. 通过o 分配,例如&amp;*o == &amp;i 仍然为真,但现在i == 5
  3. 完全禁止分配。

Assign-through 是通过将= 推送到T= 得到的行为,rebind 是一种更合理的实现,是您真正想要的(另请参阅this question,以及Reference Types 上的 Matt Calabrese 演讲)。

解释 (1) 和 (2) 之间区别的另一种方式是我们如何在外部实现两者:

// rebind
o.emplace(j);

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

Boost.Optional 文档提供了这个理由:

为分配已初始化可选引用选择了重新绑定语义,以提供初始化状态之间的一致性,即使以与裸 C++ 引用的语义缺乏一致性为代价.确实,optional&lt;U&gt; 在初始化时尽可能地表现得像 U 一样;但是在UT&amp; 的情况下,这样做会导致与左值初始化状态不一致的行为。

想象optional&lt;T&amp;&gt; 转发分配给被引用对象(从而改变被引用对象的值但不重新绑定),并考虑以下代码:

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

任务是做什么的?

如果a未初始化,答案很明确:它绑定到x(我们现在有另一个对x 的引用)。但是如果 a 已经初始化了怎么办?它会改变被引用对象的值(不管是什么);这与其他可能的情况不一致。

如果optional&lt;T&amp;&gt; 会像T&amp; 那样赋值,那么除非你的代码在赋值后能够正常工作,否则a 别名相同对象是否为b

也就是说,您必须进行区分才能保持一致。

如果在您的代码中重新绑定到另一个对象不是一个选项,那么第一次绑定很可能也不是。在这种情况下,应禁止分配给 未初始化 optional&lt;T&amp;&gt;。很有可能在这种情况下,左值必须已经初始化是一个先决条件。如果不是,那么第一次绑定是可以的,而重新绑定不是,这是 IMO 不太可能的。在这种情况下,您可以直接分配值本身,如下所示:

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

对该行应该做什么缺乏一致意味着更容易完全禁止引用,因此optionalvariant 的大部分值至少可以使其适用于 C++17 并开始有用.以后总是可以添加参考资料——或者争论是这样的。

【讨论】:

  • 解释有道理,谢谢。不过,作为一个天真的用户,我希望我的变体的行为与它当前拥有的类型完全一样。我无法重新绑定引用,因此我不希望变体在分配时重新绑定。
  • “rebind 是一个更合理的实现,是你真正想要的”我猜你的意思是一般意义上的“你”而不是我个人,因为我不希望它重新绑定。跨度>
  • @olq_plo 我同意天真的期望是 optional&lt;T&amp;&gt; 确实分配。但这对于那种类型来说是一个糟糕的选择。注意boost::optional&lt;T&amp;&gt;rebinds.
  • 不过,它是变体的合理选择,所以我也许不应该强迫可选和变体表现得一样。
  • @olq_plo 我不同意(a)它是变体的合理选择和(b)可选和变体有意义的不同。如果optional&lt;T&gt;variant&lt;nullopt, T&gt; 的语义不同,那会很混乱
猜你喜欢
  • 2020-07-28
  • 2017-03-18
  • 2017-03-05
  • 2022-01-06
  • 1970-01-01
  • 1970-01-01
  • 2019-02-17
  • 2018-07-26
相关资源
最近更新 更多