C++ templates that accept only certain types

前端 未结 14 2380
一整个雨季
一整个雨季 2020-11-22 11:37

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

public class ObservableList {
  ...
         


        
相关标签:
14条回答
  • 2020-11-22 11:55

    We can use std::is_base_of and std::enable_if:
    (static_assert can be removed, the above classes can be custom-implemented or used from boost if we cannot reference type_traits)

    #include <type_traits>
    #include <list>
    
    class Base {};
    class Derived: public Base {};
    
    #if 0   // wrapper
    template <class T> class MyClass /* where T:Base */ {
    private:
        static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
        typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
    };
    #elif 0 // base class
    template <class T> class MyClass: /* where T:Base */
        protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
    private:
        static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    };
    #elif 1 // list-of
    template <class T> class MyClass /* where T:list<Base> */ {
        static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
        typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
        typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;
    
    };
    #endif
    
    int main() {
    #if 0   // wrapper or base-class
        MyClass<Derived> derived;
        MyClass<Base> base;
    //  error:
        MyClass<int> wrong;
    #elif 1 // list-of
        MyClass<std::list<Derived>> derived;
        MyClass<std::list<Base>> base;
    //  error:
        MyClass<std::list<int>> wrong;
    #endif
    //  all of the static_asserts if not commented out
    //  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
    //  1. inner
    //  2. MyClass
    //  3. base + value_type
    }
    
    0 讨论(0)
  • 2020-11-22 11:57
    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.

    0 讨论(0)
  • 2020-11-22 11:59

    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.

    0 讨论(0)
  • 2020-11-22 11:59

    I think all prior answers have lost sight of the forest for the trees.

    Java generics are not the same as templates; they use type erasure, which is a dynamic technique, rather than compile time polymorphism, which is static technique. It should be obvious why these two very different tactics do not gel well.

    Rather than attempt to use a compile time construct to simulate a run time one, let's look at what extends actually does: according to Stack Overflow and Wikipedia, extends is used to indicate subclassing.

    C++ also supports subclassing.

    You also show a container class, which is using type erasure in the form of a generic, and extends to perform a type check. In C++, you have to do the type erasure machinery yourself, which is simple: make a pointer to the superclass.

    Let's wrap it into a typedef, to make it easier to use, rather than make a whole class, et voila:

    typedef std::list<superclass*> subclasses_of_superclass_only_list;

    For example:

    class Shape { };
    class Triangle : public Shape { };
    
    typedef std::list<Shape*> only_shapes_list;
    only_shapes_list shapes;
    
    shapes.push_back(new Triangle()); // Works, triangle is kind of shape
    shapes.push_back(new int(30)); // Error, int's are not shapes
    

    Now, it seems List is an interface, representing a sort of collection. An interface in C++ would merely be an abstract class, that is, a class that implements nothing but pure virtual methods. Using this method, you could easily implement your java example in C++, without any Concepts or template specializations. It would also perform as slow as Java style generics due to the virtual table look ups, but this can often be an acceptable loss.

    0 讨论(0)
  • 2020-11-22 12:00

    The simple solution, which no one have mentioned yet, is to just ignore the problem. If I try to use an int as a template type in a function template that expects a container class such as vector or list, then I will get a compile error. Crude and simple, but it solves the problem. The compiler will try to use the type you specify, and if that fails, it generates a compile error.

    The only problem with that is that the error messages you get are going to be tricky to read. It is nevertheless a very common way to do this. The standard library is full of function or class templates that expect certain behavior from the template type, and do nothing to check that the types used are valid.

    If you want nicer error messages (or if you want to catch cases that wouldn't produce a compiler error, but still don't make sense) you can, depending on how complex you want to make it, use either Boost's static assert or the Boost concept_check library.

    With an up-to-date compiler you have a built_in static_assert, which could be used instead.

    0 讨论(0)
  • 2020-11-22 12:04

    C++20 concept usage

    https://en.cppreference.com/w/cpp/language/constraints cppreference is giving the inheritance use case as an explicit concept example:

    template <class T, class U>
    concept Derived = std::is_base_of<U, T>::value;
     
    template<Derived<Base> T>
    void f(T);  // T is constrained by Derived<T, Base>
    

    For multiple bases I'm guessing the syntax will be:

    template <class T, class U, class V>
    concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
     
    template<Derived<Base1, Base2> T>
    void f(T);
    

    GCC 10 appears to have implemented it: https://gcc.gnu.org/gcc-10/changes.html and you can get it as a PPA on Ubuntu 20.04. https://godbolt.org/ My local GCC 10.1 did not recognize concept yet, so not sure what is going on.

    0 讨论(0)
提交回复
热议问题