仅接受某些类型的C ++模板

时间:2009-05-17 10:16:01

标签: c++ templates

在Java中,您可以定义通用类,它只接受扩展您选择的类的类型,例如:

public class ObservableList<T extends List> {
  ...
}

这是使用“extends”关键字完成的。

在C ++中是否有一些简单的等效关键字?

13 个答案:

答案 0 :(得分:98)

我建议使用Boost的static assert功能与Boost Type Traits库中的is_base_of一致:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

在其他一些更简单的情况下,您可以简单地向前声明一个全局模板,但只为有效类型定义(显式或部分特化):

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[次要编辑6/12/2013:使用声明但未定义的模板将导致链接器,而非编译器,错误消息。]

答案 1 :(得分:94)

这在C ++中通常是没有根据的,正如其他答案所指出的那样。在C ++中,我们倾向于基于除了&#34;继承自该类&#34;之外的其他约束来定义泛型类型。如果你真的想这样做,那么在C ++ 11和<type_traits>中很容易做到:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

这打破了人们在C ++中所期望的许多概念。最好使用定义自己特征的技巧。例如,也许observable_list想要接受任何类型的容器,该容器具有typedef const_iterator以及返回begin的{​​{1}}和end成员函数。如果将此限制为从const_iterator继承的类,则具有自己类型且不从list继承但提供这些成员函数和typedef的用户将无法使用您的list 1}}。

这个问题有两种解决方案,其中之一就是不限制任何东西并依赖鸭子打字。这个解决方案的一个重要方面是它涉及大量的错误,这些错误对于用户来说很难解决。另一种解决方案是定义特征以约束为满足接口要求而提供的类型。这个解决方案的最大特点是涉及额外的写作,这可能被视为烦人。但是,积极的一面是,您可以编写自己的错误消息la observable_list

为完整起见,给出了上述示例的解决方案:

static_assert

上面的示例中显示了很多概念,展示了C ++ 11的功能。一些奇怪的搜索术语是可变参数模板,SFINAE,表达式SFINAE和类型特征。

答案 2 :(得分:56)

没有人提到的简单解决方案就是忽略这个问题。如果我尝试在需要容器类(如vector或list)的函数模板中使用int作为模板类型,那么我将收到编译错误。原油和简单,但它解决了这个问题。编译器将尝试使用您指定的类型,如果失败,则会生成编译错误。

唯一的问题是你得到的错误信息很难阅读。尽管如此,这是一种非常常见的方式。标准库中充满了函数或类模板,这些模板期望模板类型具有某些行为,并且不会检查所使用的类型是否有效。

如果您想要更好的错误消息(或者如果您想要捕获不会产生编译器错误但仍然没有意义的情况),您可以根据您想要的复杂程度使用Boost的static assert或Boost concept_check库。

使用最新的编译器,您有一个built_in static_assert,可以使用它。

答案 3 :(得分:13)

据我所知,这在C ++目前是不可能的。但是,有计划在新的C ++ 0x标准中添加一个名为“concepts”的功能,该功能提供了您正在寻找的功能。这个关于C ++概念的Wikipedia article将更详细地解释它。

我知道这并不能解决您当前的问题,但有一些C ++编译器已经开始添加新标准的功能,因此可能找到已经实现了概念功能的编译器。

答案 4 :(得分:11)

我们可以使用std::is_base_ofstd::enable_if
static_assert可以删除,如果我们无法引用boost,则type_traits可以自定义实施或使用上述类

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}

答案 5 :(得分:10)

只接受从类型List派生的类型T的等价物看起来像

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};

答案 6 :(得分:7)

我认为所有先前的答案都没有看到树木的森林。

Java泛型与模板不同;他们使用类型擦除,这是动态技术,而不是编译时多态,这是静态技术。很明显为什么这两种截然不同的策略不能很好地凝聚。

不是尝试使用编译时构造来模拟运行时间,而是让我们看看extends实际做了什么:according to Stack OverflowWikipedia,extends用于表示子类化。

C ++也支持子类化。

您还会显示一个容器类,它以泛型的形式使用类型擦除,并扩展为执行类型检查。在C ++中,你必须自己做类型擦除机制,这很简单:指向超类。

让我们将它包装成一个typedef,以便更容易使用,而不是整个类,等等:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

例如:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

现在,似乎List是一个接口,代表了一种集合。 C ++中的接口只是一个抽象类,即一个只实现纯虚方法的类。使用此方法,您可以轻松地在C ++中实现Java示例,而无需任何概念或模板特化。由于虚拟表查找,它的执行速度也会像Java风格的泛型一样慢,但这通常是可以接受的损失。

答案 7 :(得分:6)

执行摘要:不要这样做。

j_random_hacker的回答告诉你 如何做到这一点。但是,我还想指出你应该这样做。模板的重点在于它们可以接受任何兼容类型,并且Java样式类型约束会破坏它。

Java的类型约束是一个bug而不是一个功能。他们在那里因为Java确实在泛型上键入了擦除,所以Java无法弄清楚如何仅根据类型参数的值来调用方法。

另一方面,C ++没有这样的限制。模板参数类型可以是与它们一起使用的操作兼容的任何类型。没有共同的基类。这类似于Python的“Duck Typing”,但是在编译时完成。

一个显示模板功能的简单示例:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

此sum函数可以对任何支持正确操作的类型的向量求和。它适用于int / long / float / double等基元,以及重载+ =运算符的用户定义数值类型。哎呀,你甚至可以使用这个函数来连接字符串,因为它们支持+ =。

不需要装箱/取消装箱原语。

请注意,它还使用T()构造T的新实例。这在使用隐式接口的C ++中是微不足道的,但在具有类型约束的Java中实际上是不可能的。

虽然C ++模板没有明确的类型约束,但它们仍然是类型安全的,并且不会使用不支持正确操作的代码进行编译。

答案 8 :(得分:5)

在纯C ++中这是不可能的,但您可以通过概念检查在编译时验证模板参数,例如:使用Boost's BCCL

答案 9 :(得分:5)

class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

确保派生类继承FooSecurity结构,并且编译器会在所有正确的位置感到不安。

答案 10 :(得分:1)

  

在C ++中是否有一些简单的等效关键字?

没有

根据您要完成的任务,可能有足够的(甚至更好的)替代品。

我查看了一些STL代码(在Linux上,我认为它是源自SGI实现的代码)。它有“概念断言”;例如,如果您需要一个理解*x++x的类型,概念断言将在无操作函数(或类似的东西)中包含该代码。它确实需要一些开销,因此将它放在一个定义取决于#ifdef debug的宏中可能是明智的。

如果子类关系确实是你想知道的,你可以在构造函数中断言T instanceof list(除了它在C ++中的拼写“不同”)。这样,您可以测试编译器无法为您检查的方法。

答案 11 :(得分:1)

此类型检查没有关键字,但您可以放入一些代码,至少会以有序的方式失败:

(1)如果希望函数模板仅接受某个基类X的参数,请将其分配给函数中的X引用。 (2)如果你想接受函数而不是原语,或者你想要以其他方式过滤类,可以在函数中调用一个(空)模板助手函数,该函数只为你想要接受的类定义。

您也可以在类的成员函数中使用(1)和(2)来强制对整个类进行这些类型检查。

你可以将它放入一些智能宏来缓解你的痛苦。 :)

答案 12 :(得分:-2)

好吧,你可以创建一个类似这样的模板:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

然而,这将隐含限制,而且您不能只提供看似列表的任何内容。还有其他方法可以限制所使用的容器类型,例如,通过使用所有容器中不存在的特定迭代器类型,但这又是隐式而非显式限制。

据我所知,在当前标准中不存在将语句Java语句镜像到其完整范围的构造。

通过在模板中使用特定的typedef,有一些方法可以限制您在编写的模板中使用的类型。这将确保为不包含该特定typedef的类型编译模板特化将失败,因此您可以选择性地支持/不支持某些类型。

在C ++ 11中,概念的引入应该会使这更容易,但我认为它不会完全符合您的要求。