Search code examples
c++gccboostoperators

ADL warning: ambiguous conversion with boost operators and SFINAE


I'm trying to understand an ambiguous conversion warning during ADL for the following piece of code:

#include <boost/operators.hpp>
#include <boost/polygon/polygon.hpp>

class Scalar
    : private boost::multiplicative< Scalar, double > {
  public:
    explicit Scalar( double val ) : mVal( val ) {}
    Scalar &operator*=(double rhs) noexcept {
        mVal *= rhs;
        return (*this);
    }

    Scalar &operator/=(double rhs) noexcept {
        mVal /= rhs;
        return (*this);
    }
  private:
    double mVal;
};

using Coordinate = int;
using Polygon = boost::polygon::polygon_with_holes_data<Coordinate>;
using Point = boost::polygon::polygon_traits<Polygon>::point_type;

template <class T, typename = std::enable_if_t<std::is_arithmetic_v<std::remove_reference_t<T>>>>
Point operator*(const Point &a, T b) noexcept {
   return Point(a.x() * b, a.y() * b);
}

int main(int argc, char *argv[]){
    Scalar a( 10 );
    int b = 10;
    Scalar a_times_b = a * b;
    return 0;
}

I get the following warning for GCC 11.2:

<source>: In function 'int main(int, char**)':
<source>:33:28: warning: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:
   33 |     Scalar a_times_b = a * b;
      |                            ^
In file included from <source>:1:
/opt/compiler-explorer/libs/boost_1_78_0/boost/operators.hpp:268:1: note: candidate 1: 'Scalar boost::operators_impl::operator*(const Scalar&, const double&)'
  268 | BOOST_BINARY_OPERATOR_COMMUTATIVE( multipliable, * )
      | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:26:7: note: candidate 2: 'Point operator*(const Point&, T) [with T = int; <template-parameter-1-2> = void; Point = boost::polygon::point_data<int>]'
   26 | Point operator*(const Point &a, T b) noexcept {
      |       ^~~~~~~~
<source>:33:12: warning: variable 'a_times_b' set but not used [-Wunused-but-set-variable]
   33 |     Scalar a_times_b = a * b;
      | 

See https://godbolt.org/z/qzfvjr86c. One way to fix this is to also inherit from boost::multiplicative< Scalar, int > and perhaps also define the operators *= and /= for int (which is technically unnecessary since we get implicit conversions from int to double).

My Confusion:

For the so called "first" there is a implicit built in int->double conversion. For the so called "second" is the compiler talking about some conversion from the Scalar class to Point? I'm not sure what this conversion chain looks like as I haven't defined any way for the Scalar class to be converted to a Point. Is there something I'm missing with the enable if? Is this some sort of bug in Boost or GCC?


Solution

  • Your free operator* in the global namespace is too open. It actively assumes that Point is not constructible from Scalar, quod non:

    You should restrict it more:

    template <
        typename P, typename S,
        typename IsPoint = typename boost::polygon::is_point_concept<
            typename boost::polygon::geometry_concept<P>::type>::type,
        typename = std::enable_if_t<IsPoint::value and std::is_arithmetic_v<S>>>
    auto operator*(P const& a, S const& b) noexcept {
        return boost::polygon::construct<Point>(a.x() * b, a.y() * b);
    }
    

    I also suggest you

    • as shown, use the point traits to construct the point instead of assuming direct construction
    • don't put operator templates in global namespace (not shown)
    • use boost::polygon::scale that already exists and likely has more safeties and/or optimization (not shown)

    Live Demo

    Live On Compiler Explorer

    #include <type_traits>
    
    struct Scalar {
        explicit Scalar(double val) : mVal(val) {}
    
    private:
        friend Scalar operator*(Scalar lhs, double rhs) noexcept {
            lhs.mVal *= rhs;
            return lhs;
        }
    
        double mVal;
    };
    
    #include <boost/polygon/polygon.hpp>
    using Coordinate = int;
    using Polygon    = boost::polygon::polygon_with_holes_data<Coordinate>;
    using Point      = boost::polygon::polygon_traits<Polygon>::point_type;
    
    template <
        typename P, typename S,
        typename IsPoint = typename boost::polygon::is_point_concept<
            typename boost::polygon::geometry_concept<P>::type>::type,
        typename = std::enable_if_t<IsPoint::value and std::is_arithmetic_v<S>>>
    auto operator*(P const& a, S const& b) noexcept {
        return boost::polygon::construct<Point>(a.x() * b, a.y() * b);
    }
    
    static_assert(std::is_arithmetic_v<double>);
    static_assert(not std::is_arithmetic_v<Scalar>);
    
    int main()
    {
        auto s = Scalar(10) * 10;
        auto p = Point(10, 20) * 42;
    }