Search code examples
c++c++17stdvariant

Implicit conversion from primitive to user-defined types in variant


I have two classes Int and Bool hat mimick the respective primitive types and should be used in a std::variant. Almost compilable example:

#include <iostream>
#include <string>
#include <variant>
using namespace std;


class Bool {
 public:
  Bool(bool val) : m_value(val) {}

  operator bool() const { return m_value; }

 private:
  bool m_value{false};
};

class Int {
 public:
  Int(int val) : m_value(val) {}

  operator int() const { return m_value; }

 private:
  int m_value{0};
};


int main() {
  using VariantT = std::variant<std::string, Bool, Int>;
  Bool b1 = true;
  VariantT v1 = b1;    // (1) works
  VariantT v2 = true;  // (2) does not work
  VariantT v3 = 1;     // (3) does not work
  return 0;
}

If I remove Bool from the VariantT, (3) works. Removing Int makes (1) and (2) working.

But when Int and Bool are present, none works:

test.cpp:32:12: error: no viable conversion from 'bool' to 'VariantT' (aka 'variant<basic_string<char>, Bool, Int>')
  VariantT v2 = true;  // (2) does not work
           ^    ~~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/variant:1365:7: note: candidate constructor not viable: no known conversion from 'bool' to 'const variant<basic_string<char>, Bool, Int> &' for 1st argument
      variant(const variant& __rhs) = default;
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/variant:1366:7: note: candidate constructor not viable: no known conversion from 'bool' to 'variant<basic_string<char>, Bool, Int> &&' for 1st argument
      variant(variant&&) = default;
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/variant:1378:2: note: candidate template ignored: requirement '18446744073709551615UL < sizeof...(_Types)' was not satisfied [with _Tp = bool, $1 = enable_if_t<sizeof...(_Types) != 0>, $2 = enable_if_t<__not_in_place_tag<bool>>]
        variant(_Tp&& __t)
        ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/variant:1388:2: note: explicit constructor is not a candidate
        variant(in_place_type_t<_Tp>, _Args&&... __args)
        ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/variant:1408:2: note: explicit constructor is not a candidate
        variant(in_place_index_t<_Np>, _Args&&... __args)

(using clang++ 16 with c++17)

I want to be able to assign primitive types to the VariantT and have them implicitly converted to Int or Bool.

Why does it not work? How can I make it work?

Thanks!


Solution

  • In C++ if a function parameter is an int, for example, you can pass in a bool value, and implicit conversion takes place. And vice versa. This results in a problem:

    VariantT v2 = true;  // (2) does not work
    

    There are two possible, different implicit conversions here.

    1. Use Bool's constructor with a true parameter to construct a Bool, and then construct the variant using your Bool.

    2. Use Int's constructor, passing true, converted to an int value of 1 to the constructor, to construct an Int, and then construct the variant with the Int.

    Neither conversion is preferrable over the other, so overload resolution fails for that reason.

    How can I make it work?

    Change something. Redesign something, do something to disable implicit conversions. One way is to mess around with the constructors to prevent them from accepting an implicit conversion, and force them to accept, strictly, the corresponding type:

    (live demo)

    #include <type_traits>
    
    // ...
    
        template<typename T, typename=std::enable_if_t<std::is_same_v<T, bool>>>
        Bool(const T &val) : m_value(val) {}
    
    // ...
    
        template<typename T, typename=std::enable_if_t<std::is_same_v<T, int>>>
        Int(const T &val) : m_value(val) {}
    

    Now, Bool's constructor fails overload resolution unless its parameter is a real, bone-fide bool, and Int's constructor fails overload resolution unless its parameter is a real, bone-fide int.

    Note that this also prevents chars, longs, and other integers from squeezing through. One possible variation, that would allow this, would be !std::is_same_v<T,bool> that you might want to try.