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


158

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

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

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

在C ++中,是否有与此关键字等同的简单名称?


已经有一个很老的问题了……我觉得这里(以及答案中)还缺少的是Java泛型真的不等同于C ++中的模板。有相似之处,但是恕我直言,应该小心地将Java解决方案直接转换为C ++,只是要意识到它们可能是为各种问题而设计的;)
idclev 463035818 '18

Answers:


104

我建议与Boost Type Traits库一起使用Boost的静态断言功能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.

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


静态断言也很好。:)
macbirdie

5
@John:恐怕专业化只会myBaseType完全匹配。在关闭Boost之前,您应该知道其中大部分是仅标头模板代码-因此,在运行时不会因为不使用的内容而浪费内存或时间。同样,您将在此处(BOOST_STATIC_ASSERT()is_base_of<>)使用的特定内容也可以仅使用声明(即没有函数或变量的实际定义)来实现,因此它们也不会占用任何空间或时间。
j_random_hacker 2011年

50
C ++ 11来了。现在我们可以使用了static_assert(std::is_base_of<List, T>::value, "T must extend list")
泗源仁

2
顺便说一句,必须使用双括号的原因是BOOST_STATIC_ASSERT是一个宏,而多余的括号阻止了预处理程序将is_base_of函数参数中的逗号解释为第二个宏参数。
jfritz42

1
@Andreyua:我不太了解缺少的内容。您可以尝试声明一个变量,my_template<int> x;或者my_template<float**> y;验证编译器是否允许这些变量my_template<char> z;,然后声明一个变量并验证它是否不允许。
j_random_hacker

134

正如这里的其他答案所指出的,这在C ++中通常是不必要的。在C ++中,我们倾向于基于“来自此类的继承”之外的其他约束来定义泛型类型。如果您真的想这样做,那么在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和a beginend返回return 的成员函数的任何类型的容器const_iterator。如果将其限制为继承自的类,list则拥有自己的类型但不继承list但提供这些成员函数和typedef的用户将无法使用您的observable_list

有两种解决方案,其中一种是不限制任何内容,而是依靠鸭子的打字。此解决方案的一个主要缺点是,它涉及大量错误,可能使用户难以理解。另一个解决方案是定义特征以约束提供的类型以满足接口要求。该解决方案的最大弊端是涉及额外的写作,这可能会令人讨厌。但是,积极的一面是您将能够编写自己的错误消息static_assert

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

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

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

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

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


2
直到今天,我才意识到C ++模板使用鸭子类型。有点奇怪!
安迪

2
鉴于C ++引入了C的广泛策略限制,因此不确定为什么会有template<class T:list>这样的冒犯性概念。谢谢你的提示。
bvj

60

目前尚无人提及的简单解决方案是忽略该问题。如果我尝试使用int在希望使用容器类(例如vector或list)的函数模板中 as作为模板类型,那么我将得到编译错误。粗略而简单,但却解决了问题。编译器将尝试使用您指定的类型,如果失败,它将生成编译错误。

唯一的问题是,您收到的错误消息将很难读取。但是,这是一种非常普通的方法。标准库充满了功能或类模板,这些模板期望模板类型具有某些行为,并且不检查所使用的类型是否有效。

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

使用最新的编译器,您可以使用built_in static_assert来代替。


7
是的,我一直认为模板是C ++中最接近鸭式打字的东西。如果它具有模板所需的所有元素,则可以在模板中使用它。

@John:对不起,我无法做到这一点。哪种类型T,该代码从何处调用?没有一些上下文,我就没有机会理解该代码片段。但是我说的是真的。如果您尝试调用toString()没有toString成员函数的类型,则会出现编译错误。
jalf

@John:下次,当您的代码中出现问题时,也许您应该减少一些令人发指的沮丧的人
jalf

@杰夫,好。+1。只是试图使其达到最佳,这是一个很好的答案。抱歉误读。我以为我们在谈论使用类型作为参数而不是函数模板的类,我认为函数模板是函数模板的成员,但是需要调用以使编译器进行标记。
约翰

13

我们可以用std::is_base_ofstd::enable_if
static_assert可以被删除,上面的类可以定制实现或者使用升压,如果我们不能参考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
}

13

据我所知,这在C ++中目前是不可能的。但是,计划在新的C ++ 0x标准中添加一个称为“概念”的功能,该功能可提供您所需要的功能。这Wikipedia文章关于C ++的概念进行详细解释。

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


4
不幸的是,概念已从标准中删除。
macbirdie

4
C ++ 20应该采用约束和概念。
Petr Javorik

static_assert如其他答案所示,即使没有概念,也可以使用和SFINAE。对于来自Java或C#或Haskell(...)的人来说,剩下的问题是C ++ 20编译器不会根据Java和C#所必需的概念进行定义检查
user7610

10

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

Java泛型与模板不同 ; 他们使用类型擦除(这是一种动态技术)而非编译时间多态性(这是静态技术)。很明显,为什么这两种截然不同的策略不能很好地融合在一起。

与其尝试使用编译时结构来模拟运行时结构,不如看一下它的extends实际作用:根据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示例,而无需任何Concepts或模板专门化。由于虚拟表查找,它的执行速度也将与Java样式的泛型一样慢,但这通常是可以接受的损失。


3
我不喜欢使用诸如“应该显而易见”或“每个人都知道”这样的短语的答案,然后继续解释什么是显而易见的或普遍已知的。显而易见的是相对于上下文,经验和经验上下文。这样的表述本质上是不礼貌的。
3Dave's

2
@DavidLively现在要批评礼节这个答案还为时已晚了两年,但在这种情况下,我也不同意你的看法。我解释了为什么两种技术表述很明显之前而不是之后没有同时使用。我提供了上下文,然后说从上下文得出的结论是显而易见的。那不完全适合您的模具。
爱丽丝

该回答的作者说,在进行一些繁重的操作后,某些事情是显而易见的。我不认为作者打算说解决方案是显而易见的。
卢克·吉桑

10

仅接受从类型列表派生的类型T的等效项看起来像

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

8

内容提要:不要那样做。

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

此求和函数可以求和支持正确运算的任何类型的向量。它可以与int / long / float / double之类的原语一起使用,也可以与用户定义的使+ =运算符重载的数字类型一起使用。哎呀,您甚至可以使用此函数来连接字符串,因为它们支持+ =。

无需对原语进行装箱/拆箱。

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

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


2
如果您建议不要专门使用模板,那么您也可以解释为什么使用该语言吗?

1
我明白了,但是如果您的模板参数必须从特定类型派生,那么比起普通的编译器错误呕吐,从static_assert获得易于解释的消息更好。
jhoffman0x

1
是的,C ++在这里更具表现力,但这通常是一件好事(因为我们可以用更少的钱表达更多的东西),有时我们想刻意限制我们赋予自己的力量,以确保我们完全理解系统。
j_random_hacker 2014年

如果您希望能够利用某些只能用于某些类型的操作,则@Curg类型专用化非常有用。例如,一个布尔值通常〜每个字节一个字节,即使一个字节通常〜可以保存8位/布尔值;模板集合类可以(并且在std :: map的情况下)专门用于布尔值,因此它可以更紧密地打包数据以节省内存。
thecoshman

另外,为了澄清起见,此答案不是说“从不专门化模板”,而是说不要使用该功能来尝试限制可以与模板一起使用的类型。
thecoshman

6

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

从C ++ 20开始,概念已成为该语言的正式功能


2
嗯,这可能的,但概念检查仍然是一个好主意。:)
j_random_hacker

实际上,我的意思是在“普通” C ++中是不可能的。;)
macbirdie's

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结构,并且编译器将在所有正确的地方感到不高兴。


@Zehelvion Type::FooSecurity用于模板类。如果传入模板参数的类没有FooSecurity,尝试使用它会导致错误。确保如果传入模板参数的类没有FooSecurity,则它不是从派生的Base
GingerPlusPlus 2014年

2

C ++ 20概念用法

https://en.cppreference.com/w/cpp/language/constraints cppreference给出了继承用例作为一个明确的概念示例:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

对于多个碱基,我猜语法将是:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

GCC 10似乎已经实现了它:https : //gcc.gnu.org/gcc-10/changes.html,您可以在Ubuntu 20.04上以PPA的形式获得它。https://godbolt.org/我本地的GCC 10.1尚未识别concept,所以不确定发生了什么。


1

在C ++中,是否有与此关键字等同的简单名称?

没有。

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

我浏览了一些STL代码(在Linux上,我认为这是从SGI的实现派生的代码)。它具有“概念断言”;例如,如果您需要一个能够理解*x和的类型++x,则概念断言将在无为函数(或类似功能)中包含该代码。确实需要一些开销,因此将其放在定义取决于的宏中可能很聪明#ifdef debug

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


1

没有用于类型检查的关键字,但是您可以放置​​一些至少有序失败的代码:

(1)如果您希望功能模板仅接受特定基类X的参数,请将其分配给功能中的X引用。(2)如果要接受函数但不接受原语(反之亦然),或者要以其他方式过滤类,请在仅为要接受的类定义的函数内调用(空)模板帮助器函数。

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

您可以将其放入一些智能宏中以减轻痛苦。:)


-2

好吧,您可以创建类似以下内容的模板:

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

但是,这将使限制变得隐含,而且您不能仅提供看起来像列表的任何内容。还有其他方法来限制使用的容器类型,例如通过使用并非在所有容器中都存在的特定迭代器类型,但这再次是隐式的而不是显式的限制。

据我所知,在当前标准中还没有一种能够完全反映Java语句的结构。

通过在模板中使用特定的typedef,可以使用多种方法来限制可在模板中使用的类型。这将确保针对不包含特定typedef的类型的模板专业化的编译将失败,因此您可以有选择地支持/不支持某些类型。

在C ++ 11中,概念的引入应该使此操作更容易,但是我认为它也不能完全满足您的要求。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.