Search code examples
c++templatesgeneric-programming

c++ template return type depending on template arguments?


I have implemented my own SI Unit class. When using arithmetic operations the resulting SI Unit can change. E.g: ( meter / second ) / meter = 1 / second.

Ok now I have also created a simple 3D Vector class. This vector shall be generic and also usable with my SI Unit class. So I implemented a simple division operator. See the following code:

// Determine result type of Lhs / Rhs:
template < class Lhs, class Rhs >
    struct TV3TypeV3Div { typedef BOOST_TYPEOF( Lhs( ) / Rhs( ) ) type; };

// Vector / Vector:
template < class Lhs, class Rhs >
RobotTools::DataTypes::TV3Type< typename TV3TypeV3Div< Lhs, Rhs >::type > operator/( const RobotTools::DataTypes::TV3Type< Lhs >& lhs,
                                                                                     const RobotTools::DataTypes::TV3Type< Rhs >& rhs )
{
    // do something useful
    return RobotTools::DataTypes::TV3Type< typename TV3TypeV3Div< Lhs, Rhs >::type >( 0, 0, 0 );
}

// Vector / Vector
RobotTools::DataTypes::TV3Type< Tools::DataTypes::Length > vl;
vl / vl;  // Ok this works

During compile time the right return type will be determined by using the TV3TypeV3Div struct. This works.

Now I want to extend the operators. I want also calculate vectors with scalar types. So I wrote this operator:

// Vector / Scalar
template < class Lhs, class Rhs >
RobotTools::DataTypes::TV3Type< typename TV3TypeV3Div< Lhs, Rhs >::type > operator/( const RobotTools::DataTypes::TV3Type< Lhs >& lhs,
                                                                                     const Rhs& rhs )
{
    // do something useful
    return RobotTools::DataTypes::TV3Type< typename TV3TypeV3Div< Lhs, Tools::DataTypes::Length >::type >( 0, 0, 0 );
}

// Vector / Scalar
RobotTools::DataTypes::TV3Type< Tools::DataTypes::Length > vl;
Tools::DataTypes::Length sl;
vl / sl;  // Ok nice it works too

So far so good. The problem is when I define the second operator (Vector/Scalar) this operator is so generic that the compiler wants to use it also for Vector/Vector division. But it fails because Lhs( ) / Rhs( ) with:

Lhs=Tools::DataTypes::Length and Rhs=RobotTools::DataTypes::TV3Type

is not defined. This is correct and I understand the given error. What I do not understand is that the compiler does NOT use the Vector/Vector operator.

  • Is there any possibility to give the compiler a hint which operator to use ?
  • Is there any possibility to rewrite the operators to fullfill my requirement ?

Solution

  • The compiler does not want to use the Vector/Scalar operator for Vector/Vector division. It just wants to make check if there's a match.

    If you declare (but not define) a completely generic division operator

    template <typename Lhs, typename Rhs>
    struct InvalidDivision {};
    template <typename Lhs, typename Rhs>
    InvalidDivision<Lhs, Rhs> 
    operator/(const Lhs& lhs, const Rhs& rhs); // do not define
    

    then your code should compile and link. The Vector/Scalar overload will be considered and rejected because Vector/Vector is a better match.

    The downside of it is if you do divide things that do not have division defined for them, the compiler will complain about InvalidDivision<something,other> instead of giving its usual error.

    Perhaps this could be improved by using SFINAE or some other advanced magic.

    Update: a more detailed explanation

    What is going on in the original version of the code?

    We are trying to invoke a division operator for a user-defined type. There are two functions named operator/ and the compiler considers both and tries to find out which one is a better match.

    First consider the Vector/Vector operator/. The compiler deduces that both Lhs and Rhs template parameters are Length (I omit namespaces for brevity). It then substitutes them into parameters of TV3TypeV3Div, computes the insides of TV3TypeV3Div, determines that TV3TypeV3Div<Lhs,Rhs>::type is also Length, and finally computes the return type of operator/ which is TV3Type<Length>.

    Now consider the Vector/Scalar operator/. The compiler deduces that Lhs is Length, but Rhs is TV3Type<Length>. It then substitutes these types into parameters of TV3TypeV3Div, and tries to compute the insides of TV3TypeV3Div. Here's where things break up: there/s no operator/ that would accept Length and TV3Type<Length>. So it is impossible to compute TV3TypeV3Div<Lhs,Rhs>::type. The compiler outputs an error.

    Now consider what happens when the generic operator/ is declared. Now there is an operator/ that would accept Length and TV3Type<Length>! (Or any other pair of arguments, for that matter). So the compiler happily computes TV3TypeV3Div<Lhs,Rhs>::type and then the return type of Vector/Scalar operator/. So now it is possible to consider both Vector/Vector and Vector/Scalar operator/ overloads.

    The compiler now also sees and considers the generic operator/ for the overload resolution. All three overloads produce a match. But the Vector/Vector overload wins, because it is more specialized then either Vector/Scalar or generic operator/, and is therefore a better match.

    Update 2

    It should be possible to do this:

    namespace Detail
    {
        template <typename Lhs, typename Rhs>
            struct DoNotUse {};
        template <typename Lhs, typename Rhs>
            DoNotUse<Lhs, Rhs> operator/(const Lhs& lhs, const Rhs& rhs);
        template < class Lhs, class Rhs >
            struct TV3TypeV3Div { typedef BOOST_TYPEOF( Lhs( ) / Rhs( ) ) type; };
    }
    using Detail::TV3TypeV3Div;
    

    This should nicely hide the generic operator/ from everyone but TV3TypeV3Div.