为什么在std::Variant中禁止引用?

2022-02-25 00:00:00 c++ variant boost c++17

我经常使用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);
}

解决方案

从根本上讲,optionalvariant不允许引用类型的原因是在赋值(在较小程度上,比较)应该为此类情况做什么方面存在分歧。optionalvariant更容易在示例中显示,因此我将坚持这一点:

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

标记行可以解释为:

  1. 重新绑定o,使得&*o == &j。作为此行的结果,ij本身的值保持不变。
  2. 通过o赋值,这样的&*o == &i仍然成立,但现在是i == 5
  3. 完全禁止分配。

直通赋值是只将=推送到T=所获得的行为,重新绑定是一种更完善的实现,也是您真正想要的(另请参阅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<U>都会尽量表现得像U一样;但是在UT&的情况下,这样做会导致行为w.r.t到左值初始化状态不一致。

设想optional<T&>将赋值转发给被引用的对象(从而更改被引用的对象值,但不重新绑定),并考虑以下代码:

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

作业做什么?

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

如果optional<T&>将像T&那样进行赋值,则如果不显式处理以前的初始化状态,您将永远无法使用Optional的赋值,除非您的代码能够正常运行,无论在赋值之后,a是否将同一对象作为b的别名。

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

如果在您的代码中没有重新绑定到另一个对象的选项,那么很可能第一次绑定也不是。在这种情况下,禁止赋值给未初始化的optional<T&>。很可能在这种情况下,必须已经初始化左值是一个前提条件。如果不是,那么第一次绑定是可以的,而重新绑定是不可以的,这在我看来是非常不可能的。在这种情况下,您可以直接赋值,如下所示:

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

对该行应该做什么缺乏一致意见意味着完全禁止引用会更容易,这样和的大多数值至少可以在C++17中使用,并开始发挥作用。在C++17中,optionalvariant的大部分值至少可以用于C++17并开始有用。引用总是可以在以后添加-或者说争论是这样进行的。

相关文章