为什么在C ++中对模板强加类型约束是不好的?

时间:2014-08-19 15:01:27

标签: c++ templates inheritance

this question中,OP询问了限制模板接受哪些类。随后的情绪摘要是Java中的等效设施是坏的;并且不要这样做。

我不明白为什么这很糟糕。鸭子打字肯定是一个强大的工具;但是在我看来,当一个类看起来 close (相同的函数名)但行为略有不同时,它会让运行时出现问题。而且你不一定要依赖于编译时检查,因为这样的例子:

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

3 个答案:

答案 0 :(得分:11)

这太长了,但可能会提供信息。

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

C ++中的

template是代码生成和模式匹配机制。

您可以使用C ++ template来完成Java泛型的工作。对于std::function< A(B) >A类型以及转换为其他Bstd::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完全不同的实现(现在这被认为是一个错误,因为实现差异&#34;泄漏&#34;在这种情况下以烦人的方式)。每个方法和函数都是为您传递的特定类型独立生成的。

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

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

问题&#34;是一个连续容器的参数&#34;是一个很好的问题。这允许任何人实现顺序容器,并且这样的顺序容器可以像手工制作的C代码一样具有高性能,具有完全不同的实现。

如果你创建了一个所有容器都继承自的公共根std::container<T>,那么它将会充满virtual表格,或者除了作为标记类型之外它将是无用的。作为标记类型,它会侵入所有非std容器,要求它们从std::container<T>继承为真正的容器。

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

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

C ++ 11更进了一步。 for( auto&& x : y )可以处理在设计基于范围的迭代之前很久就编写的内容,而不会触及类本身。您只需在类所属的命名空间中编写一个空闲的beginend函数,它返回一个有效的前向迭代器(注意:即使是无效的前向迭代器也足够接近),突然{{1开始工作。

for ( auto&& x : y )是将这些技术与类型擦除一起使用的示例。它有一个构造函数,它接受任何可以使用std::function< A(B) >复制,销毁,调用并且其返回类型可以转换为(B)的内容。它可以采用的类型可以完全不相关 - 只测试所需的类型。

由于A的设计,我们可以拥有不相关类型的lambda invokable,如果需要可以将其类型擦除为公共std::function,但是当不进行类型擦除时,它们的invokation操作可以从有类型。因此,一个带有lambda的std::function函数在调用时知道会发生什么,这使得内联操作变得简单。

这种技术并不新鲜 - 它是在C ++中的template,这是一种比C std::sort更快的高级算法,因为它可以很容易地将可调用的对象作为比较器传递出去

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

这样的机制消除了公共基类的中心责任,允许在不修改的情况下调整现有类以传递您的需求(如果合理),仅在需要的地方放置类型擦除,避免qsort开销,并且当发现属性不成立时,理想情况下会产生明显的错误。

如果您的virtual具有某些需要传递的特性,请编写一个测试这些特征的特征类。

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

如果您的类型为ENGINE,请不要求它,而是将其用作所述标签和特征以及公理类型定义的辅助工具,例如ENGINE_BASE(您永远不必继承它,它只是作为帮助者。)

避免过度指定要求。如果您的std::iterator<...>永远不会调用usually_important,那么您的Worker<X>可能在该上下文中不需要X。但是要以比#&#34;方法不能编译的方式更清晰的方式测试属性。

有时,只是平底船。遵循这样的做法可能会让事情变得更难 - 所以做一个更简单的方法。大多数代码都是编写和丢弃的。知道您的代码何时会持续存在,并且更好,更可扩展,更可维护地编写代码。知道你需要在一次性代码上练习这些技术,这样你就可以在必要时正确地编写它。

答案 1 :(得分:1)

让我转过来问你:如果Two没有被调用,为什么代码会编译为usually_important?您提供的类型满足该特定实例化的所有需求,编译器将立即告诉您特定实例化是否不再满足模板中所需功能所需的接口。

如果你坚持认为你需要一个Engine对象,那么根本不要使用模板,而是将其作为一种非模板的策略模式(使用这种方法强制执行)在编译时,用户定义的类型遵循特定的接口,而不仅仅是它看起来像一个鸭子):

class Worker
{
public:
    explicit Worker(EngineBase* data) : data_(data) {}
    void print() { cout << data_->a() << endl; }

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

private:
    EngineBase* data_;
}

int main() 
{
    Worker w(new ConcreteEngine);
    w.print();
}

答案 2 :(得分:1)

  

我不明白为什么这很糟糕。鸭子打字肯定是一个   功能强大的工具;但在我看来,这会让自己混淆运行时问题   当一个类看起来很接近(相同的函数名称)但稍微有点   不同的行为。

您可以定义一个非平凡的接口然后偶然的另一个具有不同语义但可以替换的接口是 minimal 。这永远不会发生。

  

类型二将允许Worker仅在universal_important不编译时编译   调用。

这是的事情。我们一直依赖它。它使类模板更加灵活。

匹配编译时接口严格优于运行时接口。这是因为运行时接口在编译时可以使用的关键方式(例如,接口中的不同类型)不同,并且需要一堆运行时抽象,如动态分配,这可能是不必要的。

  

在这种情况下,但是。责任在于   ENGINE的设计者,以确保它是一个有效的类型(之后他们   应该继承ENGINE_BASE)。如果他们不这样做,将会有一个编译器   错误。对我来说,这似乎更安全,而不是施加任何限制   或者增加很多额外的工作。

它并不安全。这完全没有意义。用户不小心会错误地使用错误的类型实例化类,但由于间接接口匹配,它会成功编译。

真正归结为:你应该只需要你真正需要的东西。绝对必须具备才能发挥作用。其他一切,不需要它。这是使软件可维护的核心原则。你不可能想象在我写这个课程之后很久你会想到什么恶作剧,以你从未想过的方式使用它。