Restricting templates to only certain classes?

2020-07-10 09:08发布

In Java you can restrict generics so that the parameter type is only a subclass of a particular class. This allows the generics to know the available functions on the type.

I haven't seen this in C++ with templates. So is there a way to restrict the template type and if not, how does the intellisense know which methods are available for <typename T> and whether your passed-in type will work for the templated function?

7条回答
Melony?
2楼-- · 2020-07-10 09:42

Use the static_assert with std::is_base_of

#include <type_traits>

class A {
};

class B: public A {
};

template <class T>
class Class1 {
    static_assert(std::is_base_of<A, T>::value, "T must be a descendant of A");
    ...
    T foo();
    ...
};


Class1<A> a; //it works
Class1<B> b; //it works
Class1<int> i; //compile error
查看更多
淡お忘
3楼-- · 2020-07-10 09:47

Java generics and C++ templates are completely different things, the best thing you can do is avoiding attempting a 1-to-1 mapping. That being said, the question still remains valid, and the answer is not simple.

The simple approach used in most places is that the requirements on the type are part of the contract on the template. For example, std::sort requires that the first two arguments behave as RandomAccessIterators which is a documentation only interface (the properties are described, but there is no mechanism in code for this). Then the template just uses that information.

The second simplest approach would be to document that contract and provide static_asserts that verify what can be verified.

The next available step is to use SFINAE, which is a technique in which you force the compiler to check some features of the types that are being substituted. If the substitution (and check) fails, the template is dropped as invalid and the compiler moves on. I personally discourage most uses of SFINAE, it is a great tool, but it is often misused.

In a future standard there will be higher level constructs to enforce some form of an interface on the arguments to templates by means of Concepts. The problem is that it has proven to be hard to define how those constraints are to be defined, detected or verified and until a good solution is found the standard committee won't settle for a knowingly bad approach.

All that being said, I might want to refer back to the first paragraph here. There is a huge difference between Java generics and C++ templates. The latter provides something that is called compile time polymorphism. No real interface type is defined anywhere in the program there are [in general] no constraints that the types used in a template are related in any possible way.

查看更多
你好瞎i
4楼-- · 2020-07-10 09:51

The way you'd do this in C++ would be to only use the template type in the input and output interfaces of your class.

struct base_pointer_container {
  std::vector<Base*> data;
  void append(Base* t) {
    data.push_back(t);
  }
  T* operator[](std::size_t n) const {
    Assert( n < data.size() );
    return data[n];
  }
};

template<typename T>
struct pointer_container:private base_pointer_container {
  void append(Base* t) {
    static_assert( std::is_base_of<Base,T>::value, "derivation failure" );
    return base_pointer_container::append(t);
  }
  T* operator[](std::size_t n) const {
    return static_cast<T*>(base_pointer_container::operator[](n));
  }
};

This is somewhat analogous to what Java does under the hood, if I understand their Generics properly: the Derived version is merely a thin wrapper around the Base version that does type casting on input/output operations.

C++ templates are much more than this. The entire class is written anew for each new set of type arguments, and the implementations can, in theory, be completely different. There is no relationship between a std::vector<int> and a std::vector<long> -- they are unrelated classes -- except that they can both be pattern-matched as std::vectors, and they share many properties.

This level of power is only rarely needed. But for an example of how it can be extremely powerful, most standard libraries use it to create type-erasure objects in std::function. std::function can take any language element for which operator() is defined and compatible with its signature, regardless of the run-time layout or design of the type, and erase (hide) the fact that its type is distinct.

So a function pointer, a lambda, a functor written by some programmer in tibet -- despite having no run time layout compatibilities, a std::function< bool() > can store all of them.

It (typically) does this by creating a custom holder object for each type T that has its own copy, move, construct, destruct and call code. Each of those is custom-compiled for the type in question. It then stores a copy of the object within itself, and exposes a virtual interface to each of these operations. std::function then holds a pointer to the parent interface of that custom object holder, and then exposes its "value-like" interface to the end user (you).

This mixture of pImpl and template duck type code generation and template constructors is known as type-erasure in C++, and it allows C++ to not have the common "object" root (with the entailed restrictions on run time object layout) that Java has without much in the way of sacrifice.

查看更多
走好不送
5楼-- · 2020-07-10 09:54

In C++, if you have some template argument T, then it is effectively constrained to be one of some set of classes by virtue of how it is used. For instance if you refer to T::foo somewhere in the template expansion, then T cannot possibly be a class which does not have a foo member. Or suppose T::foo does exist, but has the wrong type; your template does something like T::foo + 1, but the T::foo which is there is not arithmetic.

If T satisfies the template in every way, and the resulting instantiation makes sense, there is no reason to worry about it.

It is an important flexibility in C++ to be able to use a template with classes that are not related in any way (i.e. by inheritance).

Someone who needs to use the template just has to write a class whose structural features match the requirements of the template; the user of the template doesn't have to derive from certain types just to use them as arguments.

The only benefit to this kind of constraint would be clearer diagnosis. Rather than getting an error message about T::foo being inappropriate in some way, you might get "type Widget does not match parameter 2 of template Xyz."

However, though clearer, the diagnostic comes at the price of accepting the philosophy that this mismatch is in fact the real problem. (What if the programmer can just fix the foo member and make it work?)

查看更多
放荡不羁爱自由
6楼-- · 2020-07-10 09:55

As of C++11, there is no way to constrain template type arguments. You can, however, make use of SFINAE to ensure that a template is only instantiated for particular types. See the examples for std::enable_if. You will want to use it with std::is_base_of.

To enable a function for particular derived classes, for example, you could do:

template <class T>
typename std::enable_if<std::is_base_of<Base, T>::value, ReturnType>::type 
foo(T t) 
{
  // ...
}

The C++ working group (in particular, Study Group 8) are currently attempting to add concepts and constraints to the language. This would allow you to specify requirements for a template type argument. See the latest Concepts Lite proposal. As Casey mentioned in a comment, Concepts Lite will be released as a Technical Specification around the same time as C++14.

查看更多
家丑人穷心不美
7楼-- · 2020-07-10 10:06

Just be specific

#include <iostream>
#include <typeinfo>

template <typename T>
struct IsSpecific
{
    void name() const { std::cout << typeid(T).name() << std::endl; }
};

template<typename T> struct Specific;
template <> struct Specific<char> : public IsSpecific<char>{};
template <> struct Specific<int> : public IsSpecific<int> {};
template <> struct Specific<long> : public IsSpecific<long>{};

int main() {
    Specific<char>().name();
    Specific<int>().name();
    Specific<long>().name();
    //Specific<std::string>().name(); // fails
}
查看更多
登录 后发表回答