具有取决于模板参数的方法的模板类

Template class with method depending on template parameter

本文关键字:方法 参数 取决于      更新时间:2023-10-16

我正在编写一个旨在拍摄随机 3D 矢量的类,但我在我的项目中使用了几个几何库(一个包含在 3D 模拟中,一个包含在分析框架中,一个不包含在超过 1 GB 的框架中......这些库中的每一个都有自己的向量定义,同一方法有不同的名称,例如 getX()、GetX()、Get(0)...以获得第一个笛卡尔坐标。但有时采用通用命名约定,并且某些方法名称在两个或多个库中是相同的。
当然,我想将此代码用于这些向量中的任何一个,因此我实现了一个模板类。问题如下:我如何使我的代码适应所有这些方法名称,而无需为每个实现专门化我的类(有些共享相同的方法名称)?
我设法使用一个或另一个方法编写了一个类,现在我想推广到任意数量的方法。这样说:"如果你有方法 1,请使用这个实现,如果你有方法 2,请使用另一个,...如果你没有,那么编译错误"。

目前该类看起来像(简化为随机射击方向的部分):

// 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);
}

说:"如果你有方法 1,请使用这个实现,如果你有方法 2,使用另一个,...如果你没有,那么编译错误"。

我写了一篇文章,解释了如何在 C++11、C++14 和 C++17 中准确实现所需的内容:">使用 C++17 就地检查表达式有效性"。

我将在下面综合 C++11 和 C++14 解决方案 - 您可以使用它们通过将它们包装在一个"通用"接口中来规范化您正在处理的所有接口。然后,您可以在"通用"接口上实现算法。


假设您有:

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

并且您希望创建一个函数模板make_noise(const T& x),如果有效,则调用x.meow(),否则x.bark()有效,否则将产生编译器错误。


在 C++11 中,您可以使用enable_if检测习惯用法

您需要为要检查其存在的每个成员创建一个类型特征。例:

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 { };

下面是使用enable_if尾随返回类型的用法示例 - 此技术使用表达式 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();
}

在 C++14 中,您可以使用通用 lambdastatic_if的实现(这是我在 CppCon 2016 上关于可能的一个演讲)来使用类似命令式语法的语法执行检查。

您需要一些实用程序:

// 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>{};
}

之后,您可以在单个重载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{});
}

一些宏黑魔法if constexpr允许你在 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{};
}
}

您可以通过为操作引入自己的名称来解决此问题。为此,请创建一个 trait 类并将其专用于每个库。像这样:

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);
}

如果需要对不受支持的操作进行静态断言,可以将它们放入非专用模板中:

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");
}
};