从 uint32_t 构造的 std::variant 使用 GCC 8.2.0 比 std::optional<uint32_t> 更喜欢持有 int32_t

std::variant constructed from uint32_t prefers to hold int32_t than std::optional<uint32_t> using GCC 8.2.0

我有以下代码:

#include <variant>
#include <optional>
#include <cstdint>
#include <iostream>
#include <type_traits>

using DataType_t = std::variant<
  int32_t,
  std::optional<uint32_t>
>;

constexpr uint32_t DUMMY_DATA = 0;

struct Event
{
  explicit Event(DataType_t data)
  : data_(data)
  {}

  template <class DataType>
  std::optional<DataType> getData() const
  {
    if (auto ptr_data = std::get_if<DataType>(&data_))
    {
      return *ptr_data;
    }
    return std::nullopt;
  }

  DataType_t data_;
};

int main() {
  auto event = Event(DUMMY_DATA);
  auto eventData = event.getData<int32_t>();

  if(!eventData) {
    std::cout << "missing\n";
    return 1;
  }

  return 0;                                        
}

代码非常简单明了,但我遇到了一个奇怪的行为。 当我使用 gcc 8.2 编译它时,return 代码为 0,输出控制台上没有 'missing' 消息,这表明变体是使用 int32_t.[=17= 构造的]

另一方面,当我使用 gcc 10.2 编译它时,它的行为相反。 我试图弄清楚标准中发生了什么变化可以解释这种行为。

这里还有编译器资源管理器link:click

这是一个简化版本:

constexpr int f() {
    return std::variant<int32_t, std::optional<uint32_t>>(0U).index();
}

对于 gcc 8.3,f() == 0 但对于 gcc 10.2,f() == 1。这里的推理最终是变体初始化是......复杂的。


最初,当 C++17 发布时,从表达式 E 初始化 variant<T, U> 的方式基本上是通过重载决议来确定索引。像这样:

constexpr int __index(T) { return 0; }
constexpr int __index(U) { return 1; }

constexpr int which_index == __index(E);

在此特定示例中,T=int32_tU=optional<uint32_t>,并且 E 是类型 uint32_t 的表达式。这个重载决议会给我们 0:从 uint32_tint32_t 的转换比从 uint32_t 到 [=] 的转换 更好 [=7​​8=] 28=](前者为标准,后者为user-defined)。您可以验证这一点:

constexpr int __index(int32_t) { return 0; }
constexpr int __index(std::optional<uint32_t>) { return 1; }
static_assert(__index(0U) == 0);

但是这个规则有一些令人惊讶的结果。这在 P0608 中进行了总结,其中包括这个例子:

variant<string, bool> x = "abc";  // holds bool

这是因为到 bool 的转换仍然是标准转换,而到 string 的转换是 user-defined。这……很可能不是用户想要的。

所以新规则最终是(因为通过 P1957 进一步修改)在我们进行一轮重载决策以确定索引之前,我们首先将类型列表修剪为那些不是'缩小转换范围。也就是说,包中的那些类型 T<sub>i</sub>

Ti x[] = {E};

是一个有效的表达式。那就是 不再 bool x[] = {"abc"}; 有效,这就是 variant<string, bool> 示例现在根据需要持有 string 的原因。

但对于此处的原始示例,int32_t x[] = {u};u 是一个 uint32_t)不是有效的声明 - 这是一个缩小转换(这适用于 0U 直接,但我们在进行此检查时丢失了 constant-ness)。

一旦应用了这个缺陷报告,我们现在就有了这个重载集:

// constexpr int __index(int32_t) { return 0; } // removed from consideration
constexpr int __index(std::optional<uint32_t>) { return 1; }
static_assert(__index(0U) == 1);

这就是为什么您的 variant 现在持有 optional<uint32_t> 而不是 int32_t


The code is pretty simple and straightforward

我希望你现在认识到尝试从既不是 T 也不是 U 的类型初始化 variant<T, U> 并不简单或直接。