Why does assigning an int to a std::variant<long int, ...> fail?

962 Views Asked by At

I feel like I'm missing something obvious about int type promotion when assigning to a variant.

On gcc version 9.3.0 (Ubuntu 9.3.0-11ubuntu0~18.04.1), compiling with -std=c++17, the following code fails to compile:

#include <variant>
#include <iostream>

int main()
    {
    std::variant<long int, bool> v;  // works fine if "long" is omitted
    long int sanity = 1;             // verify that we can assign 1 to a long int; works fine

    std::cout << sizeof(sanity) << "\n";

    v = 1;                           // compiler error here: why doesn't this assign to the long int variant of v?

    return 0;
    }

Error message:

error: no match for ‘operator=’ (operand types are ‘std::variant<long int, bool>’ and ‘int’)

Is there any magic to making this work as intended, without requiring explicit casts in the assignment? Thanks!

2

There are 2 best solutions below

0
On BEST ANSWER

Assigning to a variant does not simply assign to the currently active type in the variant. Instead, the type of the right hand side is used to determine which of the possible types (the alternatives) is the best match for the right hand side. Then, that type is assigned to.

Thus, v = 1; does not automatically assign to the long int value that is already inside v, but rather, a compile-time computation is done in order to determine whether long int or bool is a better match for int (the type of the right hand side). The best match is determined using the overload resolution rules. In other words, we must imagine that two functions exist

void f(long int);
void f(bool);

and ask ourselves which function would be called by f(1). If the long int overload is selected, then v = 1 assigns to the long int object. If the bool overload is selected, then the long int that is currently inside v is destroyed and a new bool object is constructed with the value 1.

Unfortunately, this overload resolution is ambiguous: 1 requires an "integral conversion" to match either long int or bool. Thus, assigning 1 to v is a compile-time error. If the alternatives had been int and bool, then the int alternative would yield an exact match and there would be no ambiguity.

This particular example is fixed in C++20: the bool alternative is removed from consideration since a narrowing conversion would be required to initialize a bool value from an int value. Thus, in C++20, this code will always assign to the long int alternative (if there is a bool object currently inside the variant, it is destroyed).

2
On

Converting from int to both of those types has the same "distance".

It does not know which you want to assign to.

You could make a non bool boolean that refuses conversion from int.

struct boolean{
  bool value=false;
  constexpr boolean()=default;
  template<class T, std::enable_if_t<std::is_integral<T>{}, bool> =true>
  boolean(T)=delete;
  constexpr boolean(bool b):value(b){}
  constexpr boolean(boolean const&)=default;
  constexpr boolean& operator=(boolean const&)=default;
  constexpr explicit operator bool()const{return value;}
  constexpr friend bool operator==(boolean lhs, boolean rhs) { return lhs.value==rhs.value; }
  constexpr friend bool operator!=(boolean lhs, boolean rhs) { return lhs.value!=rhs.value; }
};

maybe some more operations.

Then variant<boolean, long int> won't convert from int.

Live example.