对于只接受特定类的子类的函数/类,使用模板是否被认为是“正确的”?

时间:2014-01-04 21:47:51

标签: c++ templates

让我们假设我正在制作一个游戏,并且在那个游戏中,对于从class SpawnArea继承的怪物类,有产卵点class Monster。使用模板是否正确知道我不期望任何类,Spawn只接受特定的类子集?我想使用一个模板,因为它是我知道将类类型作为参数传递以构建特定类的实例的唯一方法。有没有更优雅的方法来编写一个类的实例用于构建其他特定类的多个实例?

我读到了这个:

Make a template accept a specific class/family of classes?

它没有讨论构造一组特定类的实例的问题。

3 个答案:

答案 0 :(得分:2)

如果您必须使用模板函数(我想可以使用工厂函数更好地完成)并且您的编译器支持c ++ 11(大多数当前编译器都这样做),您可以使用type_traits来限制模板函数: / p>

#include <type_traits>
...
template <typename MonsterT,
          class = typename std::enable_if<
              std::is_base_of<Monster, MonsterT>::value
          >::type
>
std::shared_ptr<MonsterT> spawn() { ... }

这样编译器就不会接受

spawn<SomeType>() 

如果SomeType不是Monster的派生。一种更通用的解决方案是概念,但遗憾的是它们不是c ++ 11 / c ++ 14的一部分 - 有些人认为Boost Concept Check库足以满足此目的。

就像我上面所说的,使用模板函数可能不是最明智的选择(其中一个问题是倾向于阻止清晰简洁的文档) - 只是想展示一种方法来“限制”此用例的模板。 / p>

答案 1 :(得分:2)

这很常见,实际上几乎所有模板都对其参数有一定的要求。这些通常是隐式清楚模板参数的使用方式,但您可以使用type traits来改进错误消息。在C ++ 11中,可以通过#include <type_traits>从标准库中获取它们,否则可以查看Boost.TypeTraits

使用C ++ 11时,如果您还使用static_assert

,则使用非常简单
template< typename T >
std::shared_ptr< T > spawn()
{
    // make this the first line in your function to get errors early.
    // also note that you can use any compile-time check you like.
    static_assert( std::is_base_of< Monster, T >::value,
                   "T is not derived from Monster" );

    // the rest of your code follows here.
    // ...
    // return ...;
}

答案 2 :(得分:2)

我认为使用SFINAE来模拟Concepts Lite的模板约束很有用。

考虑像这样的形状类层次结构:

/* Base class. */
class Shape { /* ... */ };

/* Derived classes. */
class Circle : public Shape { /* ... */ };
class Square : public Shape { /* ... */ };
class Triangle : public Shape { /* ... */ };

查看Concepts Lite

首先,让我们看看Concepts Lite中约束的简单使用模式,类似于N3580中第2.1节中显示的示例:

/* SomeShape concept. */
template <typename T>
concept bool SomeShape() { return std::is_base_of<Shape, T>::value; }

/* T must meet SomeShape concept. */
template <SomeShape T>
double GetArea(const T &shape) { /* ... */ }

相当于

template <typename T>
requires SomeShape<T>()
double GetArea(const T &shape) { /* ... */ }

std::enable_if作为返回类型进行仿真。

现在,我们无法获得第一个显然更漂亮的表单,但我们可以使用std::enable_if模拟第二个表单。

/* SomeShape concept. */
template <typename T>
constexpr bool SomeShape() { return std::is_base_of<Shape, T>::value; }

/* Force T to be specified. */
template <bool B, typename T>
using requires = std::enable_if_t<B, T>;

可以像这样使用:

// Awkward indenting to make it look similar to the second form.
template <typename T>
requires<SomeShape<T>(),
double> GetArea(const T &shape) { /* ... */ }

多个约束

组合多个约束很容易,因为结果只需要是编译时的布尔值。假设我们没有std::is_arithmetic可用,我们可以实现它,或者如果它只是一次性使用,我们可以像这样内联使用它:

template <typename Val>
requires<std::is_integral<Val>::value || std::is_floating_point<Val>::value,
Val> DoubleMe(const Val &val) {
  return val + val;
}

std::enable_if作为返回类型

的限制
  • 不适用于没有返回类型的内容,例如构造函数。
  • 不适用于使用auto-> decltype(/* ... */)
  • 的退货类型扣除

注意std::enable_if可以进入 template-parameter-list ,使其与构造函数一起使用并返回类型推导,但它不起作用使用可变参数模板。如果这是您的首选选项,请查看my answer to this question

SFINAE与std::enable_ifstatic_assert

之间的差异

两者之间的重要区别在于,使用std::enable_if时,在重载解析期间不会考虑该函数。因此,如果你知道你不会有重载,你可能更愿意只使用static_assert,因为你会得到一个更好的错误信息(假设你当然选择了更好的错误信息)。

考虑以下重载函数:

template <typename T>
requires<SomeShape<T>(),
void> Print(const T &shape) {
  std::cout << shape << std::endl;
}

void Print(double val) {
  std::cout << std::showpoint << val << std::endl;
}

调用Print(2.0);当然会绑定到void Print(double val);重载。 Print(1);也绑定到void Print(double val);,因为void Print(const T &shape);未考虑重载解析,int可隐式转换为double

现在考虑如果我们使用static_assert会发生什么。

template <typename T>
void Print(const T &shape) {
  static_assert(SomeShape<T>(), "T must be a Shape!");
  std::cout << shape << std::endl;
}

void Print(double val) {
  std::cout << std::showpoint << val << std::endl;
}

这次调用Print(1)绑定到第一个版本,因为void Print(const T &shape);被实例化为void Print(const int &shape),这是一个比void Print(double val);更好的匹配。然后我们点击了static_assert,这给了我们一个编译时错误。