C++ templates that accept only certain types

2019-01-01 03:29发布

In Java you can define generic class that accept only types that extends class of your choice, eg:

public class ObservableList<T extends List> {
  ...
}

This is done using "extends" keyword.

Is there some simple equivalent to this keyword in C++?

标签: c++ templates
13条回答
与君花间醉酒
2楼-- · 2019-01-01 03:57

This typically is unwarranted in C++, as other answers here have noted. In C++ we tend to define generic types based on other constraints other than "inherits from this class". If you really wanted to do that, it's quite easy to do in C++11 and <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..
};

This breaks a lot of the concepts that people expect in C++ though. It's better to use tricks like defining your own traits. For example, maybe observable_list wants to accept any type of container that has the typedefs const_iterator and a begin and end member function that returns const_iterator. If you restrict this to classes that inherit from list then a user who has their own type that doesn't inherit from list but provides these member functions and typedefs would be unable to use your observable_list.

There are two solutions to this issue, one of them is to not constrain anything and rely on duck typing. A big con to this solution is that it involves a massive amount of errors that can be hard for users to grok. Another solution is to define traits to constrain the type provided to meet the interface requirements. The big con for this solution is that involves extra writing which can be seen as annoying. However, the positive side is that you will be able to write your own error messages a la static_assert.

For completeness, the solution to the example above is given:

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

There are a lot of concepts shown in the example above that showcase C++11's features. Some search terms for the curious are variadic templates, SFINAE, expression SFINAE, and type traits.

查看更多
梦该遗忘
3楼-- · 2019-01-01 04:00

That's not possible in plain C++, but you can verify template parameters at compile-time through Concept Checking, e.g. using Boost's BCCL.

查看更多
怪性笑人.
4楼-- · 2019-01-01 04:01

Executive summary: Don't do that.

j_random_hacker's answer tells you how to do this. However, I would also like to point out that you should not do this. The whole point of templates is that they can accept any compatible type, and Java style type constraints break that.

Java's type constraints are a bug not a feature. They are there because Java does type erasure on generics, so Java can't figure out how to call methods based on the value of type parameters alone.

C++ on the other hand has no such restriction. Template parameter types can be any type compatible with the operations they are used with. There doesn't have to be a common base class. This is similar to Python's "Duck Typing," but done at compile time.

A simple example showing the power of templates:

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

This sum function can sum a vector of any type that support the correct operations. It works with both primitives like int/long/float/double, and user defined numeric types that overload the += operator. Heck, you can even use this function to join strings, since they support +=.

No boxing/unboxing of primitives is necessary.

Note that it also constructs new instances of T using T(). This is trivial in C++ using implicit interfaces, but not really possible in Java with type constraints.

While C++ templates don't have explicit type constraints, they are still type safe, and will not compile with code that does not support the correct operations.

查看更多
明月照影归
5楼-- · 2019-01-01 04:02

Is there some simple equivalent to this keyword in C++?

No.

Depending on what you're trying to accomplish, there might be adequate (or even better) substitutes.

I've looked through some STL code (on linux, I think it's the one deriving from SGI's implementation). It has "concept assertions"; for instance, if you require a type which understands *x and ++x, the concept assertion would contain that code in a do-nothing function (or something similar). It does require some overhead, so it might be smart to put it in a macro whose definition depends on #ifdef debug.

If the subclass relationship is really what you want to know about, you could assert in the constructor that T instanceof list (except it's "spelled" differently in C++). That way, you can test your way out of the compiler not being able to check it for you.

查看更多
孤独总比滥情好
6楼-- · 2019-01-01 04:02

There is no keyword for such type checks, but you can put some code in that will at least fail in an orderly fashion:

(1) If you want a function template to only accept parameters of a certain base class X, assign it to a X reference in your function. (2) If you want to accept functions but not primitives or vice versa, or you want to filter classes in other ways, call a (empty) template helper function within your function that's only defined for the classes you want to accept.

You can use (1) and (2) also in member functions of a class to force these type checks on the entire class.

You can probably put it into some smart Macro to ease your pain. :)

查看更多
看淡一切
7楼-- · 2019-01-01 04:03
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;
};

Make sure derived classes inherit the FooSecurity structure and the compiler will get upset in all the right places.

查看更多
登录 后发表回答