Home > other >  Disambiguate template function specializations - value vs. reference
Disambiguate template function specializations - value vs. reference

Time:03-15

This question requires a bit of context - if you're feeling impatient, skip past the line break... I have a Vector-3,4 and Matrix-3,4 library defined in terms of template specializations; i.e., Vector<n> and Matrix<n> are defined in Matrix.hh, while non-trivial implementations (e.g., matrix multiplication, matrix inverse) have explicit specializations or instantiations in Matrix.cc for N = {3,4}.

This approach has worked well. In theory, an app could instantiate a Matrix<100>, but couldn't multiply or invert the matrix, as there are no implementation templates visible in the header. Only N = {3,4} are instantiated in Matrix.cc

Recently, I've been adding robust methods to complement any operation that involves an inner product - including matrix multiplications, matrix transforms of vectors, etc. Most 3D transforms (projections / orientations) are relatively well-conditioned, and any minor precision errors are not a problem since shared vertices / edges yield a consistent rasterization.

There are some operations that must be numerically robust. I can't do anything about how a GPU does dot products and matrix operations when rendering; but I cannot have control / camera parameters choke on valid geometry - and inner products are notorious for pathological cancellation errors, so the robust methods use compensated summation, products, dot products, etc.



This works fine for, say, Vector inner product in Matrix.hh :

////////////////////////////////////////////////////////////////////////////////
//
// inner product:


template <int n> float
inner (const GL0::Vector<n> & v0, const GL0::Vector<n> & v1)
{
    float r = v0[0] * v1[0];

    for (int i = 1; i < n; i  )
        r  = v0[i] * v1[i];

    return r; // the running sum for the inner product.
}


float
robust_inner (const GL0::Vector<3> &, const GL0::Vector<3> &);

float
robust_inner (const GL0::Vector<4> &, const GL0::Vector<4> &);


////////////////////////////////////////////////////////////////////////////////

The implementations in Matrix.cc are not trivial.

I'm in more dubious territory when adding a robust method for [A]<-[A][B] matrix multiplication; perhaps the naming is not ideal:

template <int n> GL0::Matrix<n> &
operator *= (GL0::Matrix<n> & m0, const GL0::Matrix<n> & m1);

// (external instantiation)


GL0::Matrix<3> &
robust_multiply (GL0::Matrix<3> &, const GL0::Matrix<3> &);

GL0::Matrix<4> &
robust_multiply (GL0::Matrix<4> &, const GL0::Matrix<4> &);

There is a N = {3,4} implementation for the operator *= in Matrix.cc, but it relies on the naive inner product and is not robust - though typically good enough for GL / visualization. The robust_multiply functions are also implemented in Matrix.cc.

Now of course, I want the Matrix multiplication operator:

template <int n> GL0::Matrix<n>
operator * (GL0::Matrix<n> m0, const GL0::Matrix<n> & m1) {
    return (m0 *= m1);
}

Leading me to the problematic definitions:

inline GL0::Matrix<3>
robust_multiply (GL0::Matrix<3> m0, const GL0::Matrix<3> & m1) {
    return robust_multiply(m0, m1);
}

inline GL0::Matrix<4>
robust_multiply (GL0::Matrix<4> m0, const GL0::Matrix<4> & m1) {
    return robust_multiply(m0, m1);
}

The call to robust_multiply(m0, m1) is ambiguous. Q: How can I force the LHS argument to be interpreted as a reference, ensuring a call to the previous function that modifies the (m0) argument. Obviously I can name robust_multiply as something else, but I'm more interested in utilizing the type system. I feel I'm missing something obvious in <utility> or <functional>. How do I force a call to the correct function?


(Sorry about the word count - I'm trying to clarify my own thinking as I write)

CodePudding user response:

You named robust_multiply wrong.

*= and * are fundamentally different operations. They are related, but not the same operation - different verbs.

Overloading should be used when you are doing the same operation on different nouns.

If you do that, then your problems almost certainly evaporate. Sensible overloads are easy to write.

In your case, you want to change between writing to an argument or not based on its l/r value category. That leads to ambiguity problems.

I mean, there are workarounds to your problem -- use std::ref or pointers, for example, or &, && and const& overloads -- but they are patches here.

Naming this in programming is hard. And here is a case were you should do that hard bit.

...

Now one thing you could do is bless the arguments.

template<class T>
struct robust{
  T t;
  explicit operator T&()&{return t;}
  explicit operator T()&&{
    return std::forward<T>(t);
  }
  // also get() methods
  explicit robust(T&&tin):
    t(std::forward<T>(tin))
  {}
};

then override *= and * for robust wrapped matrices.

robust{a}*=b;

(have lhs must be robust to keep overload count down).

Now the verb is clear, I just dressed up the nouns.

But this is just an idea, and not use-tested.

  • Related