将模板限制为仅某些类
Restricting templates to only certain classes?
在Java中,您可以限制泛型,以便参数类型只是特定类的子类。这允许泛型知道类型上的可用函数。
我还没有在模板C++中看到这一点。那么有没有办法限制模板类型,如果没有,智能感知如何知道哪些方法可用于<typename T>
以及传入的类型是否适用于模板化函数?
从 C++11 开始,无法约束模板类型参数。但是,您可以使用 SFINAE 来确保仅针对特定类型实例化模板。请参阅std::enable_if
的示例。您将希望将其与 std::is_base_of
.
例如,要为特定的派生类启用函数,您可以执行以下操作:
template <class T>
typename std::enable_if<std::is_base_of<Base, T>::value, ReturnType>::type
foo(T t)
{
// ...
}
C++工作组(特别是第8研究组)目前正在尝试在该语言中增加概念和限制。这将允许您指定模板类型参数的要求。请参阅最新的概念精简版提案。正如Casey在评论中提到的,Concepts Lite将与C++14同时作为技术规范发布。
将static_assert
与std::is_base_of
#include <type_traits>
class A {
};
class B: public A {
};
template <class T>
class Class1 {
static_assert(std::is_base_of<A, T>::value, "T must be a descendant of A");
...
T foo();
...
};
Class1<A> a; //it works
Class1<B> b; //it works
Class1<int> i; //compile error
Java 泛型和C++模板是完全不同的东西,你能做的最好的事情就是避免尝试一对一映射。话虽如此,这个问题仍然有效,答案并不简单。
大多数地方使用的简单方法是,对类型的要求是模板上协定的一部分。例如,std::sort
要求前两个参数表现为 RandomAccessIterators,这是一个仅文档接口(描述了属性,但代码中没有用于此的机制)。然后模板只使用该信息。
第二种最简单的方法是记录该合同并提供验证可以验证的内容的static_assert
。
下一个可用步骤是使用 SFINAE,这是一种强制编译器检查要替换的类型的某些功能的技术。如果替换(和检查)失败,则模板将被删除为无效,编译器将继续。我个人不鼓励使用SFINAE,这是一个很好的工具,但它经常被滥用。
在未来的标准中,将有更高级别的构造,通过概念在模板的参数上强制执行某种形式的接口。问题在于,事实证明很难定义如何定义、检测或验证这些约束,在找到一个好的解决方案之前,标准委员会不会满足于一个明知是坏的方法。
话虽如此,我可能想在这里回顾第一段。Java 泛型和C++模板之间存在巨大差异。后者提供了称为编译时多态性的东西。程序中的任何地方都没有定义真正的接口类型,[通常]没有约束模板中使用的类型以任何可能的方式相关。
只要具体一点
#include <iostream>
#include <typeinfo>
template <typename T>
struct IsSpecific
{
void name() const { std::cout << typeid(T).name() << std::endl; }
};
template<typename T> struct Specific;
template <> struct Specific<char> : public IsSpecific<char>{};
template <> struct Specific<int> : public IsSpecific<int> {};
template <> struct Specific<long> : public IsSpecific<long>{};
int main() {
Specific<char>().name();
Specific<int>().name();
Specific<long>().name();
//Specific<std::string>().name(); // fails
}
在C++中,如果你有一些模板参数T
,那么由于它的使用方式,它实际上被约束为一组类之一。例如,如果您在模板扩展中的某处引用T::foo
,则T
不可能是没有foo
成员的类。 或者假设T::foo
确实存在,但类型错误;您的模板执行类似 T::foo + 1
,但那里的T::foo
不是算术。
如果T
各方面都满足模板,并且生成的实例化有意义,则没有理由担心它。
模板与没有任何关系(即通过继承)相关的类一起使用是一种重要的灵活性。
需要使用模板的人只需要编写一个结构特征与模板要求匹配的类;模板的用户不必仅仅为了将它们用作参数而从某些类型派生。
这种约束的唯一好处是更清晰的诊断。您可能会收到"类型 Widget
与模板 Xyz
的参数 2 不匹配",而不是收到有关T::foo
以某种方式不合适的错误消息。
然而,尽管更清晰,但诊断是以接受这种不匹配实际上是真正的问题的哲学为代价的。(如果程序员可以修复foo
成员并使其工作怎么办?
在C++中执行此操作的方法是仅在类的输入和输出接口中使用模板类型。
struct base_pointer_container {
std::vector<Base*> data;
void append(Base* t) {
data.push_back(t);
}
T* operator[](std::size_t n) const {
Assert( n < data.size() );
return data[n];
}
};
template<typename T>
struct pointer_container:private base_pointer_container {
void append(Base* t) {
static_assert( std::is_base_of<Base,T>::value, "derivation failure" );
return base_pointer_container::append(t);
}
T* operator[](std::size_t n) const {
return static_cast<T*>(base_pointer_container::operator[](n));
}
};
如果我正确理解它们的泛型,这有点类似于 Java 在引擎盖下所做的:Derived
版本只是对输入/输出操作进行类型转换的 Base
版本的薄包装器。
C++ template
远不止于此。 整个类是针对每组新的类型参数重新编写的,理论上,实现可以完全不同。 std::vector<int>
和std::vector<long>
之间没有关系 - 它们class
es无关 - 除了它们都可以作为std::vector
进行模式匹配,并且它们共享许多属性。
这种级别的功率很少需要。 但是对于它如何非常强大的示例,大多数标准库使用它来创建 std::function
中的类型擦除对象。 std::function
可以采用定义operator()
并与其签名兼容的任何语言元素,而不考虑类型的运行时布局或设计,并擦除(隐藏)其类型不同的事实。
所以一个函数指针,一个lambda,一个由西藏的程序员编写的函子——尽管没有运行时布局兼容性,但std::function< bool() >
可以存储所有这些。
它(通常)通过为每个类型T
创建自定义 holder 对象来实现这一点,该对象具有自己的复制、移动、构造、销毁和调用代码。 其中每个都是针对相关类型自定义编译的。 然后,它将对象的副本存储在自身中,并向其中每个操作公开一个virtual
接口。 然后,std::function
持有指向该自定义对象持有者的父接口的指针,然后向最终用户(您)公开其"类似值"的接口。
这种pImpl
和template
鸭子类型代码生成和template
构造函数的混合在C++中被称为类型擦除,它允许C++没有Java具有的公共"对象"根(对运行时对象布局的必然限制),而无需牺牲太多。
C++采用的实际类型系统的演变正在进行中,它被命名为concepts
。
新的 C++1y 概念可能会提供您正在寻找的内容,并且由于此功能已经计划用于 C++11 但未进入最终草案,因此有一个gcc
分叉来实现此概念。
目前,"穷人"的解决方案,如果你想坚持标准给你的东西,就是使用type traits
。
- 没有找到相关文章