boostorg / qvm

Boost Quaternions, Vectors, Matrices library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Math functions can't be specialized to custom templated scalar types

federkamm opened this issue · comments

I'm trying to use QVM with Google Ceres which has a templated scalar type Jet<typename T, int N> for it's automatic differentiation framework. It works well for operations like vector addition and matrix multiplication using some custom traits like

template <typename T, int N>
struct deduce_scalar<S, Jet<T, N>> { using type = Jet<typename deduce_scalar<S, T>::type, N>; };

but fails for operations like mag which call math functions like sqrt in this case. In order to make it work one would have to partially specialize the boost::qvm::sqrt function to

template <typename T, int N>
Jet<T, N> sqrt<Jet<T, N>>(Jet<T, N> x) { return ceres::sqrt(x); }

but unfortunatelly, C++ doesn't allow for partial function specializations.

To fix this, I think the idiomatic way for C++ would be to use function overloading with argument dependend lookup (ADL) instead of function templating.

I.e. in the mag function T const mag=sqrt<T>(m2); should be replaced by

using std::sqrt;
T const mag=sqrt(m2);

As I understand ADL, this would look for any function of type sqrt(T) in the namespace boost::qvm, the global namespace ::, in the namespace that defined the type T, and would fallback to std::sqrt otherwise to handle the cases for double, float and the integer types (alternatively, you could define two overloaded functions boost::qvm::sqrt(double) and boost::qvm::sqrt(float) without templates and wouldn't need the using std::sqrt line).

For my case, this would work out of the box, as the type Jet of my concern is defined in namespace ceres which also contains a sqrt(Jet<T, N>) function. For other cases, one could provide an overload of sqrt in the namespace of the custom type, the global namespace, or within boost::qvm similar to how can be done for non-template types already now, just without templates.

Alternatively, you could make a class

template <typename T>
struct Sqrt {
    static T calc(T x) { return sqrt<T>(x); }
};

that falls back to the sqrt template in boost::qvm (in case anyone has already specialized it for their own types in client code) but can be partially specialized for custom templated types. Or you could make a class that specializes all math functions for one type at once:

template <typename T>
struct Math {
    static T sqrt(T x) { return boost::qvm::sqrt<T>(x); }
    static T sin(T x) { return boost::qvm::sin<T>(x); }
    // ...
};

I could probably provide a pull request but I would like you to help to decide which way to go to allow for custom templated scalar types (if you want to support them). I think I would vote for the ADL solution, but only because I imagine that this would be what an experienced C++ developer would go for. However, I wouldn't count me to that category of people, at least not regarding this type of library code.

Wow, that was fast. I can confirm that it does work for me, but I have an unrelated issue with the current master branch for which I'll open a new issue.

Currently, I think the resolution order favors boost::qvm::sqrt<float>(float) over ::sqrt(double) for float types but I think it would favor a ::sqrt(float) over the templated version, if someone included such in their project. I think a simple overload in boost::qvm::sqrt(float) instead of a template would probably prevent this and might even be favored over a global ::sqrt(float) function. However, I'm never sure about this kind of things without testing.

Can you produce an example that demonstrates the problem?

I added the example to issue #33

No, I mean an example about resolution order above. The other issue is pretty clear, I'll take a look.

I would make a test environment like this:

#include <iostream>
#include <type_traits>
namespace my {
struct W { operator double () { return 1.0; } };
int f(W) { return 0; }
int f(double) { return 1; }
int f(float) { return 2; }
int f(int) { return 3; }
template <typename T> int f(T);
//template <typename T> std::enable_if_t<std::is_floating_point<T>::value, int> f(T);
template <> int f<double>(double) { return 4; }
template <> int f<float>(float) { return 5; }
template <> int f<int>(int) { return 6; }
}
int f(double) { return 7; }
int f(float) { return 8; }
int f(int) { return 9; }
template <typename T> int f(T);
//template <typename T> std::enable_if_t<std::is_floating_point<T>::value, int> f(T);
template <> int f<double>(double) { return 10; }
template <> int f<float>(float) { return 11; }
template <> int f<int>(int) { return 12; }
namespace parent {
int f(double) { return 13; }
int f(float) { return 14; }
int f(int) { return 15; }
//template <typename T> int f(T);
template <typename T> std::enable_if_t<std::is_floating_point<T>::value, int> f(T);
template <> int f<double>(double) { return 16; }
template <> int f<float>(float) { return 17; }
//template <> int f<int>(int) { return 18; }
namespace inner {
//int f(double) { return 19; }
//int f(float) { return 20; }
//int f(int) { return 21; }
//template <typename T> int f(T);
//template <typename T> std::enable_if_t<std::is_floating_point<T>::value, int> f(T);
//template <> int f<double>(double) { return 22; }
//template <> int f<float>(float) { return 23; }
//template <> int f<int>(int) { return 24; }
int f() { return 25; }
void fun() { 
  std::cout << " " << f(1.0);
  std::cout << " " << f(1.0f);
  std::cout << " " << f(1);
  std::cout << " " << f(my::W{});
  std::cout << "\n";
}
}}
int main() {
  parent::inner::fun();
  {
    using namespace parent::inner;
    fun();
  }
  {
    using namespace parent;
    inner::fun();
  }
  {
    using namespace my;
    parent::inner::fun();
  }
  {
    using namespace my;
    using namespace parent;
    inner::fun();
  }
  return 0;
}

and analyze the situation by commenting and uncommenting different lines. My impression so far is, that it doesn't matter what namespaces are visible to the caller of fun (i.e. the four calls from main are identical), that ADL always wins (if not uncommented, the last call is always 0) and that otherwise, lookup stops in parent::inner if any symbol f is found there and than the "best" one is chosen. The overload parent::inner::f(T) would win over the template parent::inner::f<T>(T) but that doesn't matter since any f in parent::inner wins over any f outside. Therefore, your current solution seems to be more or less "safe", i.e. it always calls the boost::qvm::sqrt<T>(T) version for float and double. However, I think it would fail for integral types with linker error "couldn't find boost::qvm::sqrt<int>(int)" or similar. You might want to consider to guard your templated boost::qvm::sqrt<T> with an enable_if<is_floating_point> trait (or switch from templates to overloads).

For custom types C, I think the current solution is working. For e.g. sqrt in the same namespace as C, that function is called by ADL. If it doesn't exist, one can define it, can specialize boost::qvm::sqrt<C>(C) if C is not a template by itself, or use a overload template <typename T> boost::qvm::sqrt(C<T>) for templated types.

lookup stops in parent::inner if any symbol f is found there

Yes, the technical term for this is name hiding. It seems you're saying there's no problem with the current develop implementation (I didn't think there was).

I'm aware of the link error for integer types. The reason QVM provides math function overloads is to be able to do automatic overload resolution between e.g. std::sin/std::sinf. If QVM defined the main template qvm::sin (rather than just specializations for float and double), should it call std::sin or std::sinf? The link error forces the user to choose, which is a good thing.

Yes, I think the current develop implementation does the right thing. But I thought, sinf and sinl in std exist only for better C-code compatibility and that std::sin is supposed to choose the correct overload for float, double and long double by its own (at least it defines all three overloads). Doesn't it do it correctly?

The math functions are ANSI C. Per C++ convention, if a C++ program uses #include <math.h>, we get e.g. sin (the double version) and sinf (the float version) declared in the global namespace. If instead we #include <cmath>, we get the same functions but defined in namespace std, as to avoid polluting the global namespace, in theory. In practice the global namespace is already polluted. :)

The function templates defined by boost/qvm/math.hpp in namespace qvm serve the purpose you had in mind.

sin(double), sinf(float), and sinl(long double) are provided in global namespace in math.h for C-compatibility. Since C++11, sinf(double) and sinl(long double) are also provided within std namespace in math.h and cmath. Before, they only provided an overloaded std::sin function for double, float, long double, and IntegralTypes (don't know, who would ever need the sin of an integral number of radians only) which is supposed to choose the "best" algorithm for the datatype by overload resolution. So indeed, there is a difference between the functions in the global namespace (no overloads) and the functions provided in the std namespace (with overloads). The standard says to that:

20.2 The C standard library [library.c]

  1. The C++standard library also makes available the facilities of the C standard library, suitably adjusted to ensure static type safety.

  2. The descriptions of many library functions rely on the C standard library for the semantics of those functions. In some cases, the signatures specified in this document may be different from the signatures in the C standard library, and additional overloads may be declared in this document, but the behavior and the preconditions (including any preconditions implied by the use of an ISO C restrict qualifier) are the same unless otherwise stated.

29.9.1 Header <cmath> synopsis [cmath.syn]

// [...]
namespace std {
  // [...]
  float sin(float x); // see 20.2
  double sin(double x);
  long double sin(long double x); // see 20.2
  float sinf(float x);
  long double sinl(long double x);
  // [...]
}

News to me, thanks.