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

mga*_*mer 144 c++ templates

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

public class ObservableList<T extends List> {
  ...
}
Run Code Online (Sandbox Code Playgroud)

这是使用"extends"关键字完成的.

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

Rap*_*ptz 109

这在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..
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 如果有人想知道什么是“template&lt;typename... Args&gt;”:https://en.cppreference.com/w/cpp/language/parameter_pack (3认同)
  • 我从未意识到C++模板直到今天才使用duck typing.有点离奇! (2认同)
  • 鉴于** C **中引入了广泛的政策约束** C ++ **,因此不确定`template &lt;class T:list&gt;`为什么这么令人讨厌。谢谢你的提示。 (2认同)

j_r*_*ker 102

我建议使用Boost的静态断言功能与is_base_ofBoost Type Traits库一起使用:

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
    ...
};
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

[Minor EDIT 6/12/2013:使用声明但未定义的模板将导致链接器,而不是编译器,错误消息.]

  • C++ 11已经到来.现在我们可以使用`static_assert(std :: is_base_of <List,T> :: value,"T必须扩展列表")`. (45认同)
  • @John:我担心专业化只会匹配`myBaseType`.在解雇Boost之前,你应该知道它的大部分都是仅限标题的模板代码 - 因此在运行时没有内存或时间成本用于你不使用的东西.你在这里使用的特殊事物(`BOOST_STATIC_ASSERT()`和`is_base_of <>`)只能使用*声明*(即没有函数或变量的实际*定义*)来实现,所以它们不会带任何空间或时间. (5认同)
  • 顺便说一句,双括号是必要的原因是BOOST_STATIC_ASSERT是一个宏,额外的括号阻止预处理器将is_base_of函数参数中的逗号解释为第二个宏参数. (2认同)

jal*_*alf 58

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

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

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

使用最新的编译器,您可以使用built_in static_assert,可以使用它.

  • 是的,我一直认为模板是最接近C++中输入的东西.如果它具有模板所需的所有元素,则可以在模板中使用它. (7认同)

Bar*_*arr 13

据我所知,这在C++目前是不可能的.但是,有计划在新的C++ 0x标准中添加一个名为"concepts"的功能,该功能提供了您正在寻找的功能.这Wikipedia文章关于C++的概念进行详细解释.

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

  • 遗憾的是,概念已从标准中删除. (4认同)
  • C++ 20应该采用约束和概念. (3认同)

fir*_*rda 11

我们可以用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
}
Run Code Online (Sandbox Code Playgroud)


nh_*_*nh_ 10

只接受从类型List派生的类型T的等价物

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};
Run Code Online (Sandbox Code Playgroud)


Ali*_*ice 9

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

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
Run Code Online (Sandbox Code Playgroud)

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

  • 我不喜欢使用诸如"它应该是显而易见的"或"每个人都知道"这样的短语的答案,然后继续解释明显或普遍知晓的内容.显而易见的是与经验的背景,经验和背景有关.这些陈述本质上是粗鲁的. (3认同)
  • @DavidLively现在要批评礼节这个答案已经太迟了两年,但在这个特定情况下,我也不同意你的看法。我解释了为什么这两种技术不能一起使用,在此之前是显而易见的,而不是在之后。我提供了上下文,然后说从上下文得出的结论是显而易见的。那不完全适合您的模具。 (2认同)

cat*_*ive 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 是的,C++ 在这里更具表现力,但是虽然这通常是一件好事(因为我们可以用更少的内容表达更多的内容),但有时我们想要“故意”限制我们赋予自己的权力,以获得我们完全理解系统的确定性。 (3认同)
  • 到目前为止,我认为_因为_ Java 具有类型擦除功能,所以它实际上并不关心运行时。类型限制是对使用 API 的人进行健全性检查,告诉程序员“嘿,我期待一些符合这种接口的东西”,这样程序员就可以一眼就知道什么是有效的,而无需挖掘源代码或文档。这就是我们进行静态分析的原因:捕捉人为错误。 (3认同)
  • 如果您建议从不专门化模板,您还可以解释为什么它在语言中? (2认同)
  • 我明白你的意思,但如果你的模板参数必须从特定类型派生,那么最好有一个易于解释的来自 static_assert 的消息,而不是正常的编译器错误呕吐。 (2认同)

Cir*_*四事件 6

C++20 概念用法示例

改编自 <https://en.cppreference.com/w/cpp/language/constraints cppreference>,你可以做一些鸭子输入:

#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);
}
Run Code Online (Sandbox Code Playgroud)

编译并运行:

g++ -ggdb3 -O0 -std=c++20 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
Run Code Online (Sandbox Code Playgroud)

如果我们取消对 line 的注释// 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
Run Code Online (Sandbox Code Playgroud)

需要多个基类

如果您真的想要求某些基类之一:

#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());
}
Run Code Online (Sandbox Code Playgroud)

如果我们取消注释该行,// 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
Run Code Online (Sandbox Code Playgroud)

在 Ubuntu 21.04 GCC 10.3.0 上测试。

GCC 10 似乎已经实现了它:https : //gcc.gnu.org/gcc-10/changes.html,您可以在 Ubuntu 20.04 上将作为 PPA获取。https://godbolt.org/ GCC 10.1concept但是在 Ubuntu 20.04 上无法识别。


mac*_*die 5

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

  • 嗯,它*是*可能的,但概念检查仍然是一个好主意。:) (2认同)

Stu*_*art 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;
};
Run Code Online (Sandbox Code Playgroud)

确保派生类继承FooSecurity结构,编译器将在所有正确的位置被扰乱.