[C++] Шаблоны C ++, которые принимают только определенные типы


Answers

В C ++ это обычно необоснованно, как отметили другие ответы. В C ++ мы склонны определять общие типы, основанные на других ограничениях, отличных от «наследуемых от этого класса». Если вы действительно хотели это сделать, это довольно легко сделать в C ++ 11 и <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..
};

Это нарушает множество концепций, которые люди ожидают на C ++. Лучше использовать трюки, например, определять свои собственные черты. Например, возможно, observable_list хочет принять любой тип контейнера с параметром typedefs const_iterator и функцией begin и end member, которая возвращает const_iterator . Если вы ограничиваете это классами, которые наследуются от list то пользователь, который имеет свой собственный тип, который не наследуется от list но предоставляет эти функции-члены и typedefs, не сможет использовать ваш observable_list .

Есть два решения этой проблемы, одна из них - не сдерживать что-либо и полагаться на утиную печать. Большим преимуществом этого решения является то, что он включает в себя огромное количество ошибок, которые могут быть затруднены для пользователей. Другим решением является определение признаков для ограничения типа, предусмотренного для соответствия требованиям интерфейса. Большой конфликт для этого решения заключается в том, что он требует дополнительной записи, которая может рассматриваться как раздражающая. Однако положительная сторона заключается в том, что вы сможете написать свои собственные сообщения об ошибках a la static_assert .

Для полноты приведено решение вышеприведенного примера:

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

В приведенном выше примере представлено множество концепций, демонстрирующих возможности C ++ 11. Некоторые поисковые запросы для любопытных - это вариативные шаблоны, SFINAE, выражение SFINAE и черты типов.

Question

В Java вы можете определить общий класс, который принимает только типы, которые расширяют класс по вашему выбору, например:

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

Это делается с использованием ключевого слова «extends».

Есть ли простой простой эквивалент этому ключевому слову в C ++?




Для таких проверок типа нет ключевого слова, но вы можете поместить некоторый код, который по крайней мере потерпит неудачу:

(1) Если вы хотите, чтобы шаблон функции принимал только параметры определенного базового класса X, назначьте ему ссылку X в своей функции. (2) Если вы хотите принимать функции, но не примитивы или наоборот, или хотите фильтровать классы другими способами, вызовите (пустую) вспомогательную функцию шаблона внутри вашей функции, которая определена только для классов, которые вы хотите принять.

Вы можете использовать (1) и (2) также в функциях-членах класса, чтобы заставить эти проверки типа для всего класса.

Вы можете, вероятно, поместить его в какой-нибудь умный Macro, чтобы облегчить вашу боль. :)




Мы можем использовать std::is_base_of и std::enable_if :
( static_assert можно удалить, указанные выше классы могут быть реализованы или использоваться с помощью boost если мы не можем ссылаться на 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
}



Эквивалент, который принимает только типы T, полученные из типа List, выглядит так:

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};



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

Убедитесь, что производные классы наследуют структуру FooSecurity, и компилятор будет расстроен во всех правильных местах.




Резюме: Не делайте этого.

Ответ j_random_hacker подскажет вам, как это сделать. Однако я хотел бы также отметить, что вы не должны этого делать. Весь смысл шаблонов заключается в том, что они могут принимать любой совместимый тип, а ограничения типа стиля Java нарушают это.

Ограничения типа Java являются ошибкой, а не особенностью. Они существуют, потому что Java набирает стирание на generics, поэтому Java не может понять, как вызывать методы, основанные только на значения параметров типа.

C ++, с другой стороны, не имеет такого ограничения. Типы параметров шаблона могут быть совместимы с любыми типами операций, с которыми они используются. Не обязательно иметь общий базовый класс. Это похоже на «Duck Typing» на Python, но выполняется во время компиляции.

Простой пример, демонстрирующий мощь шаблонов:

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

Эта функция суммы может суммировать вектор любого типа, который поддерживает правильные операции. Он работает как с примитивами типа int / long / float / double, так и с определенными пользователем числовыми типами, которые перегружают оператор + =. Heck, вы можете даже использовать эту функцию для объединения строк, так как они поддерживают + =.

Никакой бокс / распаковка примитивов не требуется.

Обратите внимание, что он также создает новые экземпляры T, используя T (). Это тривиально в C ++ с использованием неявных интерфейсов, но на самом деле это невозможно в Java с ограничениями типа.

Хотя шаблоны C ++ не имеют явных ограничений типов, они по-прежнему безопасны по типу и не будут компилироваться с кодом, который не поддерживает правильные операции.