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