8
votes

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?

1
Clang gives you an error message that answers your question immediately. - n. 1.8e9-where's-my-share m.
Clang doesn't support concepts, so please clarify your statement. - user7155973
Concepts are orthogonal to this problem, you can just remove the concept declaration and replace UIntegral with typename, the result is the same. - n. 1.8e9-where's-my-share m.
Your concept definition requires std::is_integral_v<T> && !std::is_signed_v<T> to be a valid expression; it places no requirements on the value of that expression. You almost certainly want template<typename T> concept bool UIntegral = std::is_integral_v<T> && !std::is_signed_v<T>; instead. - Casey
Perfect, thanks! :) - user7155973

1 Answers

1
votes

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;
};