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

Posted

技术标签:

【中文标题】仅接受某些类型的 C++ 模板【英文标题】:C++ templates that accept only certain types 【发布时间】:2010-10-26 20:02:08 【问题描述】:

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

public class ObservableList<T extends List> 
  ...

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

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

【问题讨论】:

已经是一个很老的问题了...我觉得这里缺少的东西(也来自答案)是 Java 泛型实际上并不等同于 C++ 中的模板。有相似之处,但恕我直言,直接将 Java 解决方案转换为 C++ 时应该小心,只是要意识到它们可能是针对不同类型的问题而设计的;) 【参考方案1】:

C++20 概念使用示例

改编自 ,你可以做一些鸭式打字:

#include <cassert>
#include <concepts>

struct ClassWithMyFunc 
    int myFunc() 
        return 1;
    
;

struct ClassWithoutMyFunc ;

// Concept HasMyFunc: type 'T' has `.myFunc` and
// its return is convertible to int.
template<typename T>
concept HasMyFunc= requires(T a) 
     a.myFunc()  -> std::convertible_to<int>;
;

// Constrained function template
template<HasMyFunc T>
int f(T t) 
    return t.myFunc() + 1;


int main() 
    assert(f(ClassWithMyFunc()) == 2);
    // assert(f(ClassWithoutMyFunc()) == 2);

编译运行:

g++ -ggdb3 -O0 -std=c++20 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

如果我们取消注释 // assert(f(ClassWithoutMyFunc()) == 2); 行,它会按预期失败:

In file included from /usr/include/c++/10/cassert:44,
                 from main.cpp:1:
main.cpp: In function ‘int main()’:
main.cpp:27:34: error: use of function ‘int f(T) [with T = ClassWithoutMyFunc]’ with unsatisfied constraints
   27 |     assert(f(ClassWithoutMyFunc()) == 2);
      |                                  ^
main.cpp:21:5: note: declared here
   21 | int f(T t) 
      |     ^
main.cpp:21:5: note: constraints not satisfied
main.cpp: In instantiation of ‘int f(T) [with T = ClassWithoutMyFunc]’:
main.cpp:27:5:   required from here
main.cpp:15:9:   required for the satisfaction of ‘HasMyFunc<T>’ [with T = ClassWithoutMyFunc]
main.cpp:15:20:   in requirements with ‘T a’ [with T = ClassWithoutMyFunc]
main.cpp:16:15: note: the required expression ‘a.myFunc()’ is invalid
   16 |      a.myFunc()  -> std::convertible_to<int>;
      |       ~~~~~~~~^~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail

需要多个基类

如果您确实需要某些基类之一:

#include <concepts>
#include <type_traits>

struct Base1 ;
struct Base2 ;

struct Derived1 : public Base1 ;
struct Derived2 : public Base2 ;

struct NotDerived ;

template<typename T>
concept HasBase1Or2= std::is_base_of<Base1, T>::value || std::is_base_of<Base2, T>::value;

template<HasBase1Or2 T>
void f(T) 

int main() 
    f(Derived1());
    f(Derived2());
    // f(NotDerived());

如果我们取消注释 // f(NotDerived()); 行,它会按预期失败:

main.cpp: In function ‘int main()’:
main.cpp:22:19: error: use of function ‘void f(T) [with T = NotDerived]’ with unsatisfied constraints
   22 |     f(NotDerived());
      |                   ^
main.cpp:17:6: note: declared here
   17 | void f(T) 
      |      ^
main.cpp:17:6: note: constraints not satisfied
main.cpp: In instantiation of ‘void f(T) [with T = NotDerived]’:
main.cpp:22:19:   required from here
main.cpp:13:9:   required for the satisfaction of ‘HasBase1Or2<T>’ [with T = NotDerived]
main.cpp:13:55: note: no operand of the disjunction is satisfied
   13 | concept HasBase1Or2= std::is_base_of<Base1, T>::value ||
      |                                                 ~~~~~~^~
   14 |                      std::is_base_of<Base2, T>::value;
      |                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail

在 Ubuntu 21.04 GCC 10.3.0 上测试。

GCC 10 似乎已经实现了它:https://gcc.gnu.org/gcc-10/changes.html,你可以得到它as a PPA on Ubuntu 20.04。 https://godbolt.org/ GCC 10.1 在 Ubuntu 20.04 上无法识别 concept

【讨论】:

【参考方案2】:

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

从 C++20 开始,概念正在成为该语言的 official feature。

【讨论】:

嗯,它可能的,但概念检查仍然是一个好主意。 :) 我实际上的意思是在“普通”C++ 中这是不可能的。 ;)【参考方案3】:

这在 C++ 中通常是没有根据的,正如此处的其他答案所指出的那样。在 C++ 中,我们倾向于根据“从此类继承”以外的其他约束来定义泛型类型。如果你真的想这样做,在 C++11 和 &lt;type_traits&gt; 中很容易做到:

#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 可能想要接受任何类型的容器,该容器具有类型定义 const_iterator 以及返回 const_iteratorbeginend 成员函数。如果您将此限制为从 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 和类型特征。

【讨论】:

直到今天我才意识到 C++ 模板使用鸭子类型。有点奇怪! 考虑到 C++ 引入到 C 的广泛的策略限制,不知道为什么 template&lt;class T:list&gt; 是一个如此冒犯的概念。感谢您的提示。 如果有人想知道template&lt;typename... Args&gt;是什么:en.cppreference.com/w/cpp/language/parameter_pack【参考方案4】:

我建议将 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:使用声明但未定义的模板将导致 链接器,而不是编译器,错误消息。]

【讨论】:

静态断言也不错。 :) @John:恐怕专业化只会完全匹配myBaseType。在关闭 Boost 之前,您应该知道其中大部分是仅包含标头的模板代码——因此对于您不使用的东西在运行时没有内存或时间成本。此外,您将在此处使用的特定内容(BOOST_STATIC_ASSERT()is_base_of&lt;&gt;)可以仅使用 声明 (即没有实际的函数或变量的定义)来实现,所以它们也不会占用任何空间或时间。 C++11 来了。现在我们可以使用static_assert(std::is_base_of&lt;List, T&gt;::value, "T must extend list") 顺便说一句,双括号是必要的原因是 BOOST_STATIC_ASSERT 是一个宏,额外的括号阻止预处理器将 is_base_of 函数参数中的逗号解释为第二个宏参数。 @Andreyua:我真的不明白缺少什么。您可以尝试声明一个变量 my_template&lt;int&gt; x;my_template&lt;float**&gt; y; 并验证编译器是否允许这些,然后声明一个变量 my_template&lt;char&gt; z; 并验证它是否不允许。【参考方案5】:

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

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

【讨论】:

不幸的是,概念已从标准中删除。 C++20 应该采用的约束和概念。 正如其他答案所示,即使没有概念,使用static_assert 和 SFINAE 也是可能的。对于来自 Java 或 C# 或 Haskell(...) 的人来说,剩下的问题是 C++20 编译器不会针对所需的概念执行 definition checking,而 Java 和 C# 会这样做。【参考方案6】:

我们可以使用std::is_base_ofstd::enable_if: (static_assert 可以去掉,如果不能引用type_traits,上面的类可以自定义实现或者使用boost)

#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

【讨论】:

【参考方案7】:

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

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList

    // ...
;

【讨论】:

【参考方案8】:

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

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

与其尝试使用编译时构造来模拟运行时构造,不如让我们看看extends 的实际作用:according to Stack Overflow 和Wikipedia,extends 用于指示子类化。

C++ 也支持子类化。

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

让我们将它包装到 typedef 中,使其更易于使用,而不是制作一个完整的类,等等:

typedef std::list&lt;superclass*&gt; 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 风格的泛型一样慢,但这通常是可以接受的损失。

【讨论】:

我不喜欢使用诸如“它应该是显而易见的”或“每个人都知道”这样的短语的答案,然后继续解释什么是显而易见的或众所周知的。明显是相对于上下文、经验和经验的上下文而言的。这样的陈述本质上是粗鲁的。 @DavidLively 批评这个礼仪的答案已经晚了大约两年,但在这个具体情况下我也不同意你的看法;我解释了为什么这两种技术不能在之前结合使用,说明这是显而易见的,而不是之后。我提供了上下文,然后说从那个上下文得出的结论是显而易见的。这不完全适合你的模式。 这个答案的作者说,在做了一些繁重的工作后,有些事情是显而易见的。我不认为作者打算说解决方案是显而易见的。 这两种技术不能很好地结合使用,甚至它们必须如此,这一点并不明显,因为模板参数约束 dpm 必须与其中任何一种相同。 为什么这两种技术不能很好地协同工作,甚至它们必须如此,这一点并不明显,因为模板参数约束不必与任何一个。甚至 Strousstrup 也对 C++0x 中没有解决这个问题感到惊讶,因为这是他的首要任务。代替它提供的模板元编程是不可原谅的。指定“匹配的类必须实现这些方法(虚拟或非虚拟)”的简洁方法可以满足非 STL 程序员 99% 的需求。 (35 年以上 C++ 资深人士)【参考方案9】:

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

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

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

据我所知,在当前标准中不存在完全反映语句 Java 语句的构造。

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

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

【讨论】:

【参考方案10】:

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

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

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

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

【讨论】:

【参考方案11】:
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 派生的。【参考方案12】:

目前还没有人提到的简单解决方案就是忽略问题。如果我尝试在需要容器类(例如向量或列表)的函数模板中使用int 作为模板类型,则会出现编译错误。粗鲁和简单,但它解决了问题。编译器将尝试使用您指定的类型,如果失败,则会生成编译错误。

唯一的问题是您收到的错误消息很难阅读。然而,这是一种非常常见的方法。标准库中充满了函数或类模板,它们期望模板类型具有某些行为,并且不做任何检查使用的类型是否有效。

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

使用最新的编译器,您可以使用内置的static_assert

【讨论】:

是的,我一直认为模板是最接近 C++ 中的鸭式打字的东西。如果它具有模板所需的所有元素,则可以在模板中使用。 @John:很抱歉,我无法确定这一点。 T 是哪个类型,这个代码是从哪里调用的?没有一些上下文,我没有机会理解该代码 sn-p。但我说的是真的。如果您尝试在没有toString 成员函数的类型上调用toString(),则会出现编译错误。 @John:下一次,当问题出在代码中时,也许你应该不那么乐于对人们投反对票 @jalf,好的。 +1。这是一个很好的答案,只是试图使它成为最好的。抱歉误读。我以为我们在谈论使用类型作为类的参数而不是函数模板,我想它们是前者的成员,但需要调用编译器来标记。【参考方案13】:

执行摘要:不要那样做。

j_random_hacker 的回答告诉您如何 做到这一点。但是,我还想指出,您应该这样做。模板的全部意义在于它们可以接受任何兼容的类型,而 Java 样式类型约束打破了这一点。

Java 的类型约束是一个错误而不是一个特性。它们存在是因为 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++ 模板没有显式类型约束,但它们仍然是类型安全的,并且不会使用不支持正确操作的代码进行编译。

【讨论】:

如果您建议永远不要专门化模板,您能否也解释一下为什么它使用该语言? 我明白你的意思,但如果你的模板参数必须从特定类型派生,那么最好从 static_assert 获得易于解释的消息,而不是正常的编译器错误呕吐。 是的,C++ 在这里更具表现力,但虽然这通常是一件好事(因为我们可以用更少的东西表达更多),但有时我们想故意限制我们赋予的权力我们自己,以确定我们完全理解一个系统。 @Curg 类型专业化在您希望能够利用某些只能针对某些类型完成的事情时很有用。例如,一个布尔值~通常~每个字节,即使一个字节~通常~可以保存 8 位/布尔值;模板集合类可以(在 std::map 的情况下)专门用于布尔值,因此它可以更紧密地打包数据以节省内存。 到目前为止,我认为 因为 Java 具有类型擦除功能,它实际上并不关心运行时。类型限制是对使用 API 的人的健全性检查,告诉程序员“嘿,我期待符合这种接口的东西”,这样程序员就可以一眼就知道什么是有效的,而无需挖掘源代码或文档。这就是我们进行静态分析的原因:捕捉人为错误。【参考方案14】:

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

没有。

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

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

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

【讨论】:

以上是关于仅接受某些类型的 C++ 模板的主要内容,如果未能解决你的问题,请参考以下文章

C++ 模板详解(转)

C++模板

C++模板简介

C++ template<typename> 模板怎么用

如何使我的 C++ 类事件系统成为仅限于类型数组的模板类?

C ++参数包,仅限于具有单一类型的实例?