Search code examples
c++templatesimplicit-conversionc++-conceptsc++20

Template deduction vs. implicit user-defined conversion operator


I tried to implement a little example of user-defined type conversion involving templates.

#include <cassert>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <type_traits>

template <typename T>
concept bool UIntegral = requires() {
    std::is_integral_v<T> && !std::is_signed_v<T>;
};

class Number
{
public:
    Number(uint32_t number): _number(number)
    {
        if (number == 1) {
            number = 0;
        }
        
        for (; number > 1; number /= 10);
        if (number == 0) {
            throw std::logic_error("scale must be a factor of 10");
        }
    }
    
    template <UIntegral T>
    operator T() const
    {
        return static_cast<T>(this->_number);
    }
        
private:
    uint32_t _number;
};

void changeScale(uint32_t& magnitude, Number scale)
{
    //magnitude *= scale.operator uint32_t();
    magnitude *= scale;
}

int main()
{
    uint32_t something = 5;
    changeScale(something, 100);
    std::cout << something << std::endl;

    return 0;
}

I get the following compilation error (from GCC 7.3.0):

main.cpp: In function ‘void changeScale(uint32_t&, Number)’:

main.cpp:40:15: error: no match for ‘operator*=’ (operand types are ‘uint32_t {aka unsigned int}’ and ‘Number’)

magnitude *= scale;

Notice the line commented out - this one works:

//magnitude *= scale.operator uint32_t();

Why can't the templated conversion operator be automatically deduced? Thanks in advance for help.

[EDIT]

I followed the advice of removing concepts to use Clang and see its error messages. I got the following (this is truncated but sufficient):

main.cpp:34:15: error: use of overloaded operator '*=' is ambiguous (with operand types 'uint32_t'
  (aka 'unsigned int') and 'Number')
magnitude *= scale;
~~~~~~~~~ ^  ~~~~~
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, float)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, double)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, long double)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, __float128)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, int)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, long)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, long long)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, __int128)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned int)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned long)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned long long)
main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned __int128)

So, having the concepts turned on I assume that the only way to cast a Number is to do it to an unsigned integral type - then why is it insufficient for the compiler to deduce the conversion?


Solution

  • The requires concept expression works like SFINAE, it only checks that the expression is valid, but does not evaluate it.

    To have the concept actually restrict T to an unsigned integral type, use a bool expression:

    template<typename T>
    concept bool UIntegral = std::is_integral_v<T> && !std::is_signed_v<T>;
    

    Will that fix your issue though? Unfortunately not, read on...

    Why can't the templated conversion operator be automatically deduced?

    Writing buggy C++ code is a sure way to hit a compiler bug :-) There are over 1,000 confirmed unresolved bugs in gcc.

    Yes the templated conversion operator should be found, and the "no match for 'operator*='" error message should be instead "ambiguous overload for 'operator*='".

    So, having the concepts turned on I assume that the only way to cast a Number is to do it to an unsigned integral type - then why is it insufficient for the compiler to deduce the conversion?

    Even if the concept requirement and the compiler bug were fixed, the ambiguity will remain, specifically these four:

    main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned int)
    main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned long)
    main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned long long)
    main.cpp:34:15: note: built-in candidate operator*=(unsigned int &, unsigned __int128)
    

    That's because there are a lot of built-in operators for every conceivable promoted built-in type, and int, long, long long and __int128 are all integral types.

    For that reason it's usually not a good idea to templatize a conversion to a built-in type.

    Solution 1. Make the conversion operator template explicit and request the conversion explicitly

        magnitude *= static_cast<uint32_t>(scale);
        // or
        magnitude *= static_cast<decltype(magnitude)>(scale);
    

    Solution 2. Just implement a non-templated conversion to the type of _number:

    struct Number
    {
        using NumberType = uint32_t;
        operator NumberType () const
        {
            return this->_number;
        }
        NumberType _number;
    };