gpt4 book ai didi

c++ - 为什么在C++中对模板施加类型约束很不好?

转载 作者:行者123 更新时间:2023-12-03 18:00:10 24 4
gpt4 key购买 nike

this question中,OP询问了如何限制模板接受的类。随之而来的观点总结是,Java中的等效功能很糟糕;不要这样做

我不明白为什么这很糟糕。鸭子打字无疑是一个强大的工具。但是在我看来,当类看起来很接近(函数名称相同)但行为略有不同时,它会给自己带来困惑的运行时问题。由于以下示例,您不一定要依赖编译时检查:

struct One { int a; int b };
struct Two { int a; };

template <class T>
class Worker{
T data;

void print() { cout << data.a << endl; }

template <class X>
void usually_important () { int a = data.a; int b = data.b; }
}

int main() {
Worker<Two> w;
w.print();
}

类型 Two仅在未调用 Worker的情况下才允许 usually_important进行编译。这可能会导致 Worker编译的某些实例化,甚至导致不在同一程序中的实例化。

但是在这种情况下。责任由 ENGINE的设计者承担,以确保它是有效的类型(在此之后,他们应该继承 ENGINE_BASE)。如果不这样做,将出现编译器错误。对我来说,这似乎更安全,同时不施加任何限制或增加很多其他工作。
class ENGINE_BASE {}; // Empty class, all engines should extend this

template <class ENGINE>
class NeedsAnEngine {
BOOST_STATIC_ASSERT((is_base_of<ENGINE_BASE, ENGINE>));
// Do stuff with ENGINE...
};

最佳答案

这太长了,但可能会提供引用。

Java中的泛型是一种类型擦除机制,可以自动生成类型转换和类型检查的代码。

C++中的template是代码生成和模式匹配机制。

您可以使用C++的template来完成Java泛型的工作。 std::function< A(B) >AB类型以及转换为其他std::function< X(Y) >方面表现出协变/相反的方式。

但是两者的主要设计并不相同。

Java List<X>将是上面带有一些薄包装的List<Object>,因此用户不必在提取时进行类型转换。如果将其作为List<? extends Bar>传递,则本质上又会获得List<Object>,它仅具有一些额外的类型信息,这些信息会更改强制转换的工作方式以及可以调用的方法。这意味着您可以将List中的元素提取到Bar中并知道它可以工作(并检查)。所有List<? extends Bar>仅生成一种方法。

C++ std::vector<X>本质上不是std::vector<Object>std::vector<void*>或其他任何东西。 C++ template的每个实例都是不相关的类型(模板模式匹配除外)。实际上,std::vector<bool>使用的实现与其他任何std::vector完全不同(现在被认为是错误的,因为在这种情况下,实现会以令人讨厌的方式“泄漏”)。每个方法和函数都是针对您传递给它的特定类型而独立生成的。

在Java中,假定所有对象都适合某个层次结构。在C++中,这有时很有用,但是已经发现它通常不适合解决问题。

C++容器不需要从公共(public)接口(interface)继承。 std::list<int>std::vector<int>是不相关的类型,但是您可以对它们进行统一操作-它们都是顺序容器。

问题“参数是否为顺序容器”是一个好问题。这使任何人都可以实现顺序容器,并且这种顺序容器可以与完全不同的实现方式像手工制作的C代码一样具有高性能。

如果您创建了一个公共(public)根std::container<T>,所有容器都从该根目录继承而来,那么它要么充满virtual表屑,要么除了用作标记类型外,它将无用。作为标记类型,它将以侵入方式将其自身注入(inject)所有非std容器,要求它们从std::container<T>继承为真实容器。

相反,特征方法意味着有关于容器(顺序容器,关联容器等)的规范。您可以在编译时测试这些规范,和/或允许类型通过某种特征来说明它们符合某些公理的条件。

C++ 03/11标准库使用迭代器执行此操作。 std::iterator_traits<T>是一个traits类,它公开有关任意类型T的迭代器信息。完全不连接标准库的人可以编写自己的迭代器,并使用std::iterator<...>自动处理std::iterator_traits,手动添加自己的类型别名或专门设置std::iterator_traits来传递所需的信息。

C++ 11更进一步。 for( auto&& x : y )可以处理在设计基于范围的迭代之前就编写的东西,而无需接触类本身。您只需在该类所属的 namespace 中编写一个免费的beginend函数,该函数会返回有效的正向迭代器(注意:即使是足够接近工作的无效正向迭代器),然后for ( auto&& x : y )也会突然开始工作。
std::function< A(B) >是将这些技术与类型擦除结合使用的示例。它具有一个构造函数,该构造函数可以接受可以使用(B)复制,销毁和调用的任何内容,并且其返回类型可以转换为A。它可以采用的类型可以完全不相关-仅测试所需的类型。

由于std::function的设计,我们可以有不相关类型的lambda可调用项,可以在需要时将其类型擦除为通用的std::function,但是如果不进行类型擦除,则可以从那里知道它们的调用 Action 。因此,采用lambda的template函数在调用时就知道会发生什么,这使内联变得容易。

这项技术不是什么新技术,它是C++中的一种语言,因为std::sort是一种高级算法,由于易于内联作为比较器传递的可调用对象,因此它比C的qsort更快。

简而言之,如果您需要通用的运行时类型,请键入“擦除”。如果需要某些属性,请测试这些属性,不要强加共同基础。如果需要某些公理来保存(不可修改的属性),请记录文档或要求调用者通过标签或特征类声明这些属性(请参阅标准库如何处理迭代器类别-再次,而不是继承)。如有疑问,请使用启用了ADL的自由函数来访问参数的属性,并让您的默认自由函数使用SFINAE查找方法并调用(如果存在),否则将失败。

这种机制消除了常见基类的主要责任,允许对现有类进行修改而无需修改以通过您的要求(如果合理),仅将类型擦除放置在需要的地方,避免virtual开销,并且理想情况下在属性时生成清晰的错误被发现不成立。

如果您的ENGINE有某些特性需要通过,请编写一个测试这些特性的traits类。

如果存在无法测试的属性,请创建描述此类属性的标签。使用traits类或规范的typedef的特殊化,让该类描述针对该类型的公理。 (请参见迭代器标签)。

如果您有类似ENGINE_BASE的类型,请不要使用它,而应将其用作上述标记和特征以及std::iterator<...>这样的公理typedef的帮助器(您不必继承它,它只是充当帮助器)。

避免过度指定要求。如果usually_important从未在Worker<X>上调用,则在这种情况下,您的X可能不需要b。但是,以比“方法无法编译”更清晰的方式测试属性。

有时,只是平底锅。遵循这样的做法可能会使您的工作变得更困难-因此,请采用一种更简单的方法。大多数代码被编写并丢弃。知道您的代码何时可以持久保存,并更好,更可扩展,更可维护地编写代码。知道您需要在一次性代码上练习这些技术,以便在需要时可以正确地编写它们。

关于c++ - 为什么在C++中对模板施加类型约束很不好?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25386765/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com