Home > Enterprise >  std::variant constructed from uint32_t prefers to hold int32_t than std::optional<uint32_t> us
std::variant constructed from uint32_t prefers to hold int32_t than std::optional<uint32_t> us

Time:03-19

I've got the following code:

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

The code is pretty simple and straightforward but I've encountered a weird behavior. When I compile it using gcc 8.2, the return code is 0 and there is no 'missing' message on the output console, which indicates that the variant was constructed using int32_t.

On the other hand, when I compile it using gcc 10.2 it behaves the opposite way. I'm trying to figure out what has changed in standard which would explain this behavior.

Here is also compiler explorer link: click

CodePudding user response:

Here's a reduced version:

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

For gcc 8.3, f() == 0 but for gcc 10.2, f() == 1. The reasoning here is ultimately that variant initialization is... complicated.


Originally, when C 17 shipped, the way that initializing a variant<T, U> from an expression E worked was basically by way of overload resolution to determine the index. Something like this:

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

constexpr int which_index == __index(E);

In this particular example, T=int32_t and U=optional<uint32_t>, and E is an expression of type uint32_t. This overload resolution would give us 0: the conversion from uint32_t to int32_t is better than the conversion from uint32_t to optional<uint32_t> (former is standard, latter is user-defined). You can verify this:

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

But this rule has some surprising results. This was summarized in P0608, which included this example:

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

This is because the conversion to bool is still a standard conversion, while the conversion to string was user-defined. Which is... very unlikely to be what the user intended.

So the new rule ended up being (since modified further by way of P1957) that before we do the round of overload resolution to determine the index, we first prune the list of types to those that aren't narrowing conversions. That is, those types Ti from the pack for which:

Ti x[] = {E};

is a valid expression. That is not valid anymore for bool x[] = {"abc"};, which is why the variant<string, bool> example now holds a string as desired.

But for the original example here, int32_t x[] = {u}; (for u being an uint32_t) is not a valid declaration - this is a narrowing conversion (this would work for 0U directly, but we lose the constant-ness by the time we get to this check).

Once this defect report was applied, we now have this overload set:

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

Which is why your variant now holds an optional<uint32_t> rather than an int32_t.


The code is pretty simple and straightforward

I hope by now you recognize that attempting to initialize a variant<T, U> from a type that is neither T nor U isn't exactly simple or straightforward.

  • Related