为什么在C ++中对模板施加类型约束很不好? [英] Why is it bad to impose type constraints on templates in C++?

查看:80
本文介绍了为什么在C ++中对模板施加类型约束很不好?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

此问题中,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();
}

如果未调用usually_important,则

类型Two将允许Worker仅编译 .这可能导致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 ++容器不需要从公共接口继承. std::list<int>std::vector<int>是不相关的类型,但是您可以对它们进行统一操作-它们都是顺序容器.

参数是否为顺序容器"是一个很好的问题.这样一来,任何人都可以实现顺序容器,而这种顺序容器可以实现与完全不同的实现的手工C代码一样的高性能.

如果您创建了一个公共根目录std::container<T>,所有容器都从该根目录继承而来,那么它要么充满virtual表碎片,要么除了用作标记类型外都将变得无用.作为标记类型,它将侵入性地将其自身插入所有非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 )可以处理在设计基于范围的迭代之前就编写的东西,而无需接触类本身.您只需在该类所属的名称空间中编写一个免费的beginend函数,该函数将返回一个有效的正向迭代器(注意:即使是足够接近工作的无效正向迭代器),突然地for ( auto&& x : y )也将开始工作. /p>

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

由于std::function的设计,我们可以拥有不相关类型的lambda可调用商品,可以在需要时将其类型擦除为常见的std::function,但是在不进行类型擦除时,可以从那里知道它们的调用动作.因此,使用lambda的template函数在调用时就知道会发生什么,从而使内联变得容易,可以进行本地操作.

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

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

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

如果您的ENGINE具有某些特质需要通过,请编写一个测试这些特质的特质类.

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

如果您有类似ENGINE_BASE的类型,请不要使用它,而是将其用作上述标记和特征以及公理typedef的帮助者,例如std::iterator<...>(您不必继承它,它就简单地充当助手).

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

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

In this question the OP asked about limiting what classes a template will accept. A summary of the sentiment that followed is that the equivalent facility in Java is bad; and don't do this.

I don't understand why this is bad. Duck typing is certainly a powerful tool; but in my mind it lends itself confusing runtime issues when a class looks close (same function names) but has slightly different behavior. And you can't necessarily rely on compile time checking because of examples like this:

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();
}

Type Two will allow Worker to compile only if usually_important is not called. This could lead to some instantiations of Worker compiling and others not even in the same program.

In a case like this, though. The responsibility is put on to the designer of ENGINE to ensure that it is a valid type (after which they should inherit ENGINE_BASE). If they don't, there will be a compiler error. To me this seems much safer while not imposing any restrictions or adding much additional work.

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

解决方案

This is too long, but it might be informative.

Generics in Java are a type erasure mechanism, and automatic code generation of type casts and type checks.

templates in C++ are code generation and pattern matching mechanisms.

You can use C++ templates to do what Java generics do with a bit of effort. std::function< A(B) > behaves in a covariant/contravariant fashion with regards to A and B types and conversion to other std::function< X(Y) >.

But the primary design of the two is not the same.

A Java List<X> will be a List<Object> with some thin wrapping on it so users don't have to do type casts on extraction. If you pass it as a List<? extends Bar>, it again is getting a List<Object> in essence, it just has some extra type information that changes how the casts work and which methods can be invoked. This means you can extract elements from the List into a Bar and know it works (and check it). Only one method is generated for all List<? extends Bar>.

A C++ std::vector<X> is not in essence a std::vector<Object> or std::vector<void*> or anything else. Each instance of a C++ template is an unrelated type (except template pattern matching). In fact, std::vector<bool> uses a completely different implementation than any other std::vector (this is now considered a mistake because the implementation differences "leak" in annoying ways in this case). Each method and function is generated independently for the particular type you pass it.

In Java, it is assumed that all objects will fit into some hierarchy. In C++, that is sometimes useful, but it has been discovered it is often ill fitting to a problem.

A C++ container need not inherit from a common interface. A std::list<int> and std::vector<int> are unrelated types, but you can act on them uniformly -- they both are sequential containers.

The question "is the argument a sequential container" is a good question. This allows anyone to implement a sequential container, and such sequential containers can as high performance as hand-crafted C code with utterly different implementations.

If you created a common root std::container<T> which all containers inherited from, it would either be full of virtual table cruft or it would be useless other than as a tag type. As a tag type, it would intrusively inject itself into all non-std containers, requiring that they inherit from std::container<T> to be a real container.

The traits approach instead means that there are specifications as to what a container (sequential, associative, etc) is. You can test these specifications at compile time, and/or allow types to note that they qualify for certain axioms via traits of some kind.

The C++03/11 standard library does this with iterators. std::iterator_traits<T> is a traits class that exposes iterator information about an arbitrary type T. Someone completely unconnected to the standard library can write their own iterator, and use std::iterator<...> to auto-work with std::iterator_traits, add their own type aliases manually, or specialize std::iterator_traits to pass on the information required.

C++11 goes a step further. for( auto&& x : y ) can work with things that where written long before the range-based iteration was designed, without touching the class itself. You simply write a free begin and end function in the namespace that the class belongs to that returns a valid forward iterator (note: even invalid forward iterators that are close enough work), and suddenly for ( auto&& x : y ) starts working.

std::function< A(B) > is an example of using these techniques together with type erasure. It has a constructor that accepts anything that can be copied, destroyed, invoked with (B) and whose return type can be converted to A. The types it can take can be completely unrelated -- only that which is required is tested for.

Because of std::functions design, we can have lambda invokables that are unrelated types that can be type-erased into a common std::function if needed, but when not type erased their invokation action is known from there type. So a template function that takes a lambda knows at the point of invokation what will happen, which makes inlining an easy local operation.

This technique is not new -- it was in C++ since std::sort, a high level algorithm that is faster than C's qsort due to the ease of inlining invokable objects passed as comparators.

In short, if you need a common runtime type, type erase. If you need certain properties, test for those properties, don't force a common base. If you need certain axioms to hold (untestable properties), either document or require callers to claim those properties via tags or traits classes (see how the standard library handles iterator categories -- again, not inheritance). When in doubt, use free functions with ADL enabled to access properties of your arguments, and have your default free functions use SFINAE to look for a method and invoke if it exists, and fail otherwise.

Such a mechanism removes the central responsibility of a common base class, allows existing classes to be adapted without modification to pass your requirements (if reasonable), places type erasure only where it is needed, avoids virtual overhead, and ideally generates clear errors when properties are found to not hold.

If your ENGINE has certain properites it needs to pass, write a traits class that tests for those.

If there are properties that cannot be tested for, create tags that describe such properties. Use specialization of a traits class, or canonical typedefs, to let the class describe which axioms hold for the type. (See iterator tags).

If you have a type like ENGINE_BASE, don't demand it, but instead use it as a helper for said tags and traits and axiom typedefs, like std::iterator<...> (you never have to inherit from it, it simply acts as a helper).

Avoid over specifying requirements. If usually_important is never invoked on your Worker<X>, probably your X doesn't need a b in that context. But do test for properties in a way clearer than "method does not compile".

And sometimes, just punt. Following such practices might make things harder for you -- so do an easier way. Most code is written and discarded. Know when your code will persist, and write it better and more extendably and more maintainably. Know that you need to practice those techniques on disposable code so you can write it correctly when you have to.

这篇关于为什么在C ++中对模板施加类型约束很不好?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆