gaFF
gaFF

Reputation: 757

Template class with method depending on template parameter

I am writing a class designed to shoot random 3D vectors, but I use several geometric libraries in my projects (one included in the 3D simulation, one included in the analysis framework, one which is not included in a more-than-1-GB framework...). Each of these libraries has its own vector definition, with different names for the same method, such has getX(), GetX(), Get(0)... to get the first Cartesian coordinate. But sometimes a common naming convention has been adopted and some method names are the same across two or more libraries.
Of course I want to use this code for any of these vectors, so I implemented a template class. The problem is the following: how do I adapt my code to all these method names, without specializing my class for each implementation (some share the same method names) ?
I managed to write a class using a method or another, now I would like to generalize to any number of method. Something which says: "If you have method 1, use this implementation, if you have method 2, use this other one,... and if you have none, then compilation error".

Currently the class looks like (reduced to the part shooting a random direction):

// First some templates to test the presence of some methods
namespace detail_rand {
  // test if a class contains the "setRThetaPhi" method
  template<class T>
  static auto test_setRThetaPhi(int) -> 
    decltype(void(std::declval<T>().setRThetaPhi(0.,0.,0.)),
             std::true_type{});

  template<class T>
  static auto test_setRThetaPhi(float)->std::false_type;
}

// true_type if the class contains the "setRThetaPhi" method
template<class T>
struct has_setRThetaPhi : decltype(detail_rand::test_setRThetaPhi<T>(0)) {};


// The actual class
template<class vector>
class Random
{
// everything is static for easy use, might change later
private:
  Random() = delete;
  Random(Random&) = delete;

  // the distribution, random generator and its seed
  static decltype(std::chrono::high_resolution_clock::now().time_since_epoch().count()) theSeed;
  static std::default_random_engine theGenerator;
  static std::uniform_real_distribution<double> uniform_real_distro;

// Shoot a direction, the actual implementation is at the end of the file
private: // the different implementations
  static const vector Dir_impl(std::true_type const &);
  static const vector Dir_impl(std::false_type const &);

public: // the wrapper around the implementations
  inline static const vector Direction() {
    return Dir_impl(has_setRThetaPhi<vector>());
  }
};


/// initialisation of members (static but template so in header)
// the seed is not of cryptographic quality but here it's not relevant
template<class vector> 
decltype(std::chrono::high_resolution_clock::now().time_since_epoch().count())
Random<vector>::theSeed = 
  std::chrono::high_resolution_clock::now().time_since_epoch().count();

template<class vector>
std::default_random_engine Random<vector>::theGenerator(theSeed);

template<class vector>
std::uniform_real_distribution<double> Random<vector>::uniform_real_distro(0.,1.);


/// Implementation of method depending on the actual type of vector
// Here I use the "setRThetaPhi" method
template<class vector>
const vector Random<vector>::Dir_impl(std::true_type const &)
{
  vector v;
  v.setRThetaPhi(1.,
                 std::acos(1.-2.*uniform_real_distro(theGenerator)),
                 TwoPi()*uniform_real_distro(theGenerator));
  return std::move(v);
}

// Here I use as a default the "SetMagThetaPhi" method
// but I would like to test before if I really have this method,
// and define a default implementation ending in a compilation error
// (through static_assert probably)
template<class vector>
const vector Random<vector>::Dir_impl(std::false_type const &)
{
  vector v;
  v.SetMagThetaPhi(1.,
                   std::acos(1.-2.*uniform_real_distro(theGenerator)),
                   TwoPi()*uniform_real_distro(theGenerator));
  return std::move(v);
}

Upvotes: 2

Views: 3099

Answers (2)

Vittorio Romeo
Vittorio Romeo

Reputation: 93274

Something which says: "If you have method 1, use this implementation, if you have method 2, use this other one,... and if you have none, then compilation error".

I wrote an article that explains how to implement exactly what you need in C++11, C++14 and C++17: "checking expression validity in-place with C++17".

I will synthesize the C++11 and C++14 solutions below - you can use them to normalize all the interfaces you're dealing with by wrapping them inside a single "common" one. You can then implement your algorithms on the "common" interface.


Assume that you have:

struct Cat { void meow() const; };
struct Dog { void bark() const; };

And you want to create a function template make_noise(const T& x) that calls x.meow() if valid, otherwise x.bark() if valid, otherwise produces a compiler error.


In C++11, you can use enable_if and the detection idiom.

You will need to create a type trait for every member you wish to check the existence of. Example:

template <typename, typename = void>
struct has_meow : std::false_type { };

template <typename T>
struct has_meow<T, void_t<decltype(std::declval<T>().meow())>>
    : std::true_type { };

Here's an usage example using enable_if and trailing return types - this technique makes use of expression SFINAE.

template <typename T>
auto make_noise(const T& x)
    -> typename std::enable_if<has_meow<T>{}>::type
{
    x.meow();
}

template <typename T>
auto make_noise(const T& x)
    -> typename std::enable_if<has_bark<T>{}>::type
{
    x.bark();
}

In C++14, you can use generic lambdas and an implementation of static_if (here's a talk I gave at CppCon 2016 about a possible one) to perform the check with an imperative-like syntax.

You need a few utilities:

// Type trait that checks if a particular function object can be 
// called with a particular set of arguments.
template <typename, typename = void>
struct is_callable : std::false_type { };

template <typename TF, class... Ts>
struct is_callable<TF(Ts...),
    void_t<decltype(std::declval<TF>()(std::declval<Ts>()...))>>
    : std::true_type { };

// Wrapper around `is_callable`.
template <typename TF>
struct validity_checker
{
    template <typename... Ts>
    constexpr auto operator()(Ts&&...) const
    {
        return is_callable<TF(Ts...)>{};
    }
};

// Creates `validity_checker` by deducing `TF`.
template <typename TF>
constexpr auto is_valid(TF)
{
    return validity_checker<TF>{};
}

After that, you can perform all of your checks inside a single overload of make_noise:

template <typename T>
auto make_noise(const T& x)
{
    auto has_meow = is_valid([](auto&& x) -> decltype(x.meow()){ });
    auto has_bark = is_valid([](auto&& x) -> decltype(x.bark()){ });

    static_if(has_meow(x))
        .then([&x](auto)
            {
                x.meow();
            })
        .else_if(has_bark(x))
        .then([&x](auto)
            {
                x.bark();
            })
        .else_([](auto)
            {
                // Produce a compiler-error.
                struct cannot_meow_or_bark;
                cannot_meow_or_bark{};
            })(dummy{});
}

Some macro black magic and if constexpr allow you to write this in C++17:

template <typename T>
auto make_noise(const T& x)
{
    if constexpr(IS_VALID(T)(_0.meow()))
    {
        x.meow();
    }
    else if constexpr(IS_VALID(T)(_0.bark()))
    {
        x.bark();
    }
    else
    {
        struct cannot_meow_or_bark;
        cannot_meow_or_bark{};
    }
}

Upvotes: 3

You could solve this by introducing your own names for the operations. Do this by creating a trait class and specialising it for each of the libraries. Something like this:

template <class Vector>
struct VectorTraits;

template <>
struct VectorTraits<Lib1::Vector>
{
  static auto getX(const Lib1::Vector &v) { return v.GetX(); }
  // ... etc.
};

template <>
struct VectorTraits<Lib2::Vector>
{
  static auto getX(const Lib2::Vector &v) { return v.Get(0); }
  // ... etc.
};

//Usage:

template <class vector>
auto norm2(const vector &v)
{
  using V = VectorTraits<vector>;
  return V::getX(v) * V::getX(v) + V::getY(v) + V::getY(v);
}

If you want static assertions for the unsupported operations, you can put them into the unspecialised template:

template <class T>
struct False : std::false_type {};

template <class Vector>
struct VectorTraits
{
  static void getX(const Vector &)
  {
    static_assert(False<Vector>::value, "This type does not support getting x");
  }
};

Upvotes: 0

Related Questions