Have been writing an arithmetic wrapper that could help detecting over/underflow errors, however stuck with a rather devious problem in the process.
Suppose we have a class, that handles everything that is able to cause an overflow via some overloaded operators and is implicitly castable to the underlying type for everything else. This example contains only a binary plus operator:
template<typename T_>
class Wrapper
{
public:
Wrapper(T_ val_) : m_value(val_) { } // converting constructor
operator T_(void) const { return m_value; } // underlying type conversion
// some other methods
// binary plus operators:
template<typename U_>
const Wrapper<decltype(T_() + U_())> operator +(U_ val_) const
{
// supposed to handle 'Wrapped + Unwrapped' case
return m_value + val_;
}
template<typename U_>
const Wrapper<decltype(T_() + U_())> operator +(Wrapper<U_> other_) const
{
// supposed to handle 'Wrapped + Wrapped' case
return m_value + other_.m_value;
}
template<typename U0_, typename U1_>
friend const Wrapper<decltype(U0_() + U1_())> operator +(U0_ val_, Wrapper<U1_> wrapper_)
{
// supposed to handle 'Unwrapped + Wrapped' case
return val_ + wrapper_.m_value;
}
private:
T_ m_value;
};
This (if I didn't miss something while pasting it here) compiles fine and works as expected in situations like these (every possible one of them, basically):
Wrapper<int> val = 3.14f;
::std::cout << val + 42 << ::std::endl; // Wrapped + Unwrapped
::std::cout << 42 + val << ::std::endl; // Unwrapped + Wrapped
::std::cout << val + val << ::std::endl; // Wrapped + Wrapped
However, whenever I try to make an alias for the decltype(...)
part of either the 'Wrapped + Unwrapped' or the 'Unwrapped + Wrapped' for example like this:
template<typename T0_, typename T1_>
struct Result
{
typedef decltype(T0_() + T1_()) Type;
};
template<typename T_>
class Wrapper
{
//...
template<typename U_>
const Wrapper<typename Result<T_, U_>::Type> operator +(U_ val_) const
//...
template<typename U0_, typename U1_>
friend const Wrapper<typename Result<U0_, U1_>::Type> operator +(U0_ val_, Wrapper<U1_> wrapper_)
//...
};
the 'Wrapped + Wrapped' example doesn't want to compile, because the overload resolution appears to change towards the undesired variant. It throws an error about unavailability of a default constructor for Wrapper<int>
, impying the attempt to use either the 'Wrapped + Unwrapped' or 'Unwrapped + Wrapped', both of which are not suited to handle the case in question properly.
And that confuses me greatly as it looks like change in a return type leads to a change in an overload resolution behavior. Will appreciate any advice regarding the matter.
Here's roughly how overload resolution works:
If step 2 triggers a hard error - by forming an invalid construct outside the immediate context of the function template signature - then your program is ill-formed; you never get to step 3. It doesn't matter if the candidate wouldn't have been chosen in step 3 if the error wasn't there.
Here, decltype(T_() + U_())
was in the immediate context originally. Therefore, when U_
got deduced to Wrapper<...>
and substituted into the signature, that expression is ill-formed, but the error is in the immediate context and so it's a substitution failure. But when you move that expression into a separate class template Result
, the error is no longer in the immediate context of the function template's signature, so it's a hard error instead.
If you don't want to repeat the expression multiple times, use an alias template:
template<class T, class U>
using result = decltype(T() + U());