C++ – How to Select Conversion Operators in C++

c++

The following is a C++ snippet that demonstrates a class Foo that wraps a std::optional<double>, providing conversion operators for both double and std::optional<double>.

#include <iostream>
#include <optional>

class Foo {
public:
  operator double() const {
    std::cout << "Foo::operator double()" << std::endl;
    return {};
  }

  operator std::optional<double>() const {
    std::cout << "Foo::operator std::optional<double>()" << std::endl;
    return {};
  }
};

int main() {
  Foo foo;
  std::optional<double> value;
  
  // Outputs "Foo::operator double()"
  value = foo; 
}

Surprisingly to me, when assigning an instance of Foo to a std::optional<double>, the conversion operator for double is called instead of the conversion operator for std::optional<double>.

When I replace std::optional with my own custom Optional class, as you can see below, the compiler selects the other conversion operator.

#include <iostream>

template<class T>
class Optional {
public:
  T val;
  
  Optional() = default;
  
  Optional(T) {
    std::cout << "Optional(T)" << std::endl;
  }
  
  Optional(Optional&&) noexcept {
    std::cout << "Optional(Optional&&)" << std::endl;
  }

  Optional(const Optional<T>&) {
    std::cout << "Optional(const Optional&)" << std::endl;
  }

  Optional& operator=(Optional&&) noexcept {
    std::cout << "Optional::operator=(Optional&&)" << std::endl;
    return *this;
  }

  operator const T& () const {
    std::cout << "Optional::operator const T&()" << std::endl;
    return val;
  }
};

class Foo {
public:
  operator double() const {
    std::cout << "Foo::operator double()" << std::endl;
    return {};
  }

  operator Optional<double>() const { 
    std::cout << "Foo::operator Optional<double>()" << std::endl;
    return {};
  }
};

int main() {
  Foo foo;
  Optional<double> value;

  // Outputs
  //   Foo::operator Optional<double>()
  //   Optional::operator=(Optional&&)
  value = foo;
}

Why does the compiler select a different conversion operator in each scenario?

Best Answer

Here is the cppreference list of assignment operator overloads. The only direct match is number 4:

template< class U = T >
optional& operator=( U&& value );

It only participates in overload if the following requirements satisfied:

  1. Perfect-forwarded assignment: depending on whether *this contains a value before the call, the contained value is either direct-initialized from std::forward(value) or assigned from std::forward(value). The function does not participate in overload resolution unless std::decay_t(until C++20) std::remove_cvref_t(since C++20) is not std::optional, std::is_constructible_v<T, U> is true, std::is_assignable_v<T&, U> is true, and at least one of the following is true: T is not a scalar type; std::decay_t is not T.

All requirements are satisfied with U being Foo. After that overload is chosen only Foo::operator double() const can assign to double value in optional<double>

Related Question