std::optional如何从初始化列表中就地构造std::variant?

6
#include <optional>
#include <variant>
#include <utility>
#include <set>

int main()
{
    std::optional<std::variant<std::pair<int, int>, std::set<int>>> foo(std::in_place, {1, 4});
}
  1. 这个编译通过了。但是这怎么可能呢,因为 std::variant 没有一个以 std::initializer_list<T> 作为其第一个参数的构造函数,但是 std::optional 的这个构造函数重载只是将初始化列表传递并将其他参数转发到内部类型构造函数?
  2. std::variant 现在持有哪种类型,std::set 还是 std::pair
1个回答

4

这有点复杂,请耐心阅读。

optional<T>有一个构造函数,接受一个in_place_tinitializer_list<U>(还可以有可选的其他参数)。然后它会尝试按照下面的语句构造T

T t(list, ...);

这里的 list 是初始化列表,... 表示额外的参数。

T 当然是一个 variant<pair,set> 类型。 variant 通过模板构造函数隐式转换为任何类型 V。但是,只有当类型 V 满足以下条件时,此构造函数才存在:给定该类型的值 v,以下条件成立:

W w(std::forward<V>(v));

其中Wvariant中的一种类型。现在variant可以具有多种类型,因此variant基本上考虑了所有可能的W及其构造函数重载。只要在所有可能的W中,重载分辨选择恰好一个这样的构造函数,转换就会起作用。 pair没有接受initializer_list的构造函数,因此它的任何构造函数都不计数。而set<X>则有一个接受initializer_list<X>的构造函数。
所以回到原始语句。由于optional使用了一个推断出Yinitializer_list<Y>参数,花括号初始化列表将会把Y推断为int。而intvariant中的set是相同类型的。因此,set<int>可以从initializer_list<int>构造。
因此,在variant中创建的就是set<int>
如果你的variant中有一个vector<int>,由于调用不明确,会导致编译错误。
而且,没有真正的方式通过in_place技巧来修复这个问题。你不能使用(std::in_place, std::in_place_type<std::set>, {1, 4}),因为花括号初始化列表无法被正确推断。所以你需要使用(std::in_place, std::set{1, 4}),并让移动构造从临时对象中将值移动到variant中。

很好的解释。对于我们这些凡人来说,可读性的代码就是这样了。 - Paul Sanders
@PaulSanders: 实际上,这只是各种决策的汇合,这些决策在单独考虑时是有意义的,但一起交互时会产生意外的结果。optional<T>in_place 构造应允许访问任何 T 构造函数,包括初始化列表构造函数。variant<Ts> 应该可以从这些 Ts 中的任何一个构造,或从可转换为其中任何一个 Ts 的类型中转换。具有 initializer_list 构造函数的类型可以从 initializer_list 转换。将它们结合起来,就是您得到的结果。 - Nicol Bolas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接