Шаблоны C++, которые принимают только определенные типы
В Java вы можете определить универсальный класс, который принимает только те типы, которые расширяют класс по вашему выбору, например:
public class ObservableList<T extends List> {
...
}
Это делается с помощью ключевого слова extends.
Есть ли какой-нибудь простой эквивалент этого ключевого слова в C++?
13 ответов
Я предлагаю использовать функцию статического подтверждения Boost совместно с is_base_of
из библиотеки Boost Type Traits:
template<typename T>
class ObservableList {
BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
...
};
В некоторых других, более простых случаях вы можете просто объявить глобальный шаблон в прямом направлении, но только определить (явно или частично специализировать) его для допустимых типов:
template<typename T> class my_template; // Declare, but don't define
// int is a valid type
template<> class my_template<int> {
...
};
// All pointer types are valid
template<typename T> class my_template<T*> {
...
};
// All other types are invalid, and will cause linker error messages.
[Незначительное изменение 6/12/2013: использование объявленного, но не определенного шаблона приведет к сообщениям об ошибках компоновщика, а не компилятора.]
Как правило, это неоправданно в 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
функция-член, которая возвращает const_iterator
, Если вы ограничите это классами, которые наследуются от list
затем пользователь, который имеет свой собственный тип, который не наследует от list
но предоставляет эти функции-члены и typedefs не сможет использовать ваш observable_list
,
Есть два решения этой проблемы, одно из которых - ничего не ограничивать и полагаться на типизацию утки. Большим минусом этого решения является то, что оно включает в себя огромное количество ошибок, которые могут быть сложными для пользователей. Другое решение состоит в том, чтобы определить признаки, чтобы ограничить тип, обеспеченный, чтобы удовлетворить требованиям интерфейса. Большой минус для этого решения заключается в том, что он требует дополнительной записи, что может показаться раздражающим. Тем не менее, положительным моментом является то, что вы сможете писать свои собственные сообщения об ошибках а-ля 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 и черты типа.
Простое решение, о котором еще никто не упомянул, - просто игнорировать проблему. Если я попытаюсь использовать int
как тип шаблона в шаблоне функции, который ожидает контейнерный класс, такой как вектор или список, тогда я получу ошибку компиляции. Грубо и просто, но это решает проблему. Компилятор попытается использовать указанный вами тип, и если это не удастся, он выдаст ошибку компиляции.
Единственная проблема заключается в том, что сообщения об ошибках, которые вы получите, будут сложно читать. Тем не менее, это очень распространенный способ сделать это. Стандартная библиотека полна шаблонов функций или классов, которые ожидают определенного поведения от типа шаблона, и ничего не делают для проверки правильности используемых типов.
Если вам нужны более приятные сообщения об ошибках (или если вы хотите отлавливать случаи, которые не приводят к ошибке компилятора, но все же не имеют смысла), вы можете, в зависимости от того, насколько сложным вы хотите это сделать, использовать статическое утверждение Boost или библиотека Boost concept_check.
С современным компилятором у вас есть встроенный static_assert
, который может быть использован вместо.
Мы можем использовать 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
{
// ...
};
Использование концепции C++20
https://en.cppreference.com/w/cpp/language/constraints cppreference дает пример использования наследования в качестве явного примера концепции:
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>
Я предполагаю, что для нескольких баз синтаксис будет следующим:
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, похоже, реализовал его: https://gcc.gnu.org/gcc-10/changes.html, и вы можете получить его как PPA в Ubuntu 20.04. https://godbolt.org/ Мой локальный GCC 10.1 не распозналconcept
пока не уверен, что происходит.
Насколько я знаю, в настоящее время это невозможно в C++. Однако в новом стандарте C++0x планируется добавить функцию, называемую "концепциями", которая обеспечивает требуемую функциональность. Эта статья в Википедии о C++ Concepts объяснит это более подробно.
Я знаю, что это не решает вашу непосредственную проблему, но есть некоторые компиляторы C++, которые уже начали добавлять функции из нового стандарта, так что может быть возможно найти компилятор, который уже реализовал функцию понятий.
Я думаю, что все предыдущие ответы потеряли из виду лес за деревьями.
Обобщения Java не совпадают с шаблонами; они используют стирание типа, которое является динамической техникой, а не полиморфизм времени компиляции, который является статической техникой. Должно быть очевидно, почему эти две очень разные тактики плохо склеиваются.
Вместо того, чтобы пытаться использовать конструкцию времени компиляции для имитации времени выполнения, давайте посмотрим, что extends
на самом деле это так: согласно Stack Overflow и Wikipedia, extends используется для обозначения подклассов.
C++ также поддерживает создание подклассов.
Вы также показывает контейнерный класс, который использует стирание типа в форме универсального и расширяет возможности для проверки типа. В C++ вы должны самостоятельно выполнить механизм стирания типов, что очень просто: создать указатель на суперкласс.
Давайте обернем его в typedef, чтобы его было проще использовать, а не создавать целый класс и так далее:
typedef std::list<superclass*> subclasses_of_superclass_only_list;
Например:
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
Теперь кажется, что List - это интерфейс, представляющий собой своего рода коллекцию. Интерфейс в C++ будет просто абстрактным классом, то есть классом, который реализует только чистые виртуальные методы. Используя этот метод, вы можете легко реализовать ваш пример Java в C++, без каких-либо концепций или специализаций шаблонов. Он также будет работать так же медленно, как универсальные стили в стиле Java, из-за поиска в виртуальной таблице, но это часто может быть приемлемой потерей.
Резюме: не делай этого.
Ответ j_random_hacker говорит вам, как это сделать. Однако я также хотел бы отметить, что вы не должны этого делать. Суть шаблонов в том, что они могут принимать любой совместимый тип, и ограничения типа стиля Java нарушают его.
Типовые ограничения Java - это ошибка, а не функция. Они существуют потому, что Java стирает типы в обобщенных типах, поэтому Java не может понять, как вызывать методы, основываясь только на значении параметров типа.
С ++, с другой стороны, не имеет такого ограничения. Типы параметров шаблона могут быть любого типа, совместимого с операциями, с которыми они используются. Там не должно быть общего базового класса. Это похоже на "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, так и с пользовательскими числовыми типами, которые перегружают оператор +=. Черт возьми, вы даже можете использовать эту функцию для объединения строк, так как они поддерживают +=.
Бокс / распаковка примитивов не требуется.
Обратите внимание, что он также создает новые экземпляры T, используя T(). Это тривиально в C++ с использованием неявных интерфейсов, но на самом деле это невозможно в Java с ограничениями типов.
В то время как шаблоны C++ не имеют явных ограничений типов, они по-прежнему безопасны для типов и не будут компилироваться с кодом, который не поддерживает правильные операции.
Это невозможно в простом C++, но вы можете проверить параметры шаблона во время компиляции с помощью Concept Checking, например, используя Boost BCCL.
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, и компилятор расстроится во всех нужных местах.
Есть ли какой-нибудь простой эквивалент этого ключевого слова в C++?
Нет.
В зависимости от того, что вы пытаетесь достичь, могут быть адекватные (или даже лучшие) заменители.
Я просмотрел некоторый код STL (в Linux я думаю, что он является производным от реализации SGI). Имеет "концептуальные утверждения"; например, если вам требуется тип, который понимает *x
а также ++x
утверждение концепции будет содержать этот код в функции "ничего не делать" (или что-то подобное). Это требует некоторых накладных расходов, поэтому было бы разумно поместить его в макрос, определение которого зависит от #ifdef debug
,
Если отношения с подклассом действительно то, о чем вы хотите знать, вы можете утверждать в конструкторе, что T instanceof list
(за исключением того, что это написано по-другому в C++). Таким образом, вы можете проверить свой выход из компилятора, не имея возможности проверить его за вас.
Для таких проверок типов нет ключевого слова, но вы можете поместить в него некоторый код, который, по крайней мере, упадет по порядку:
(1) Если вы хотите, чтобы шаблон функции принимал только параметры определенного базового класса X, присвойте ему ссылку X в вашей функции. (2) Если вы хотите принимать функции, но не примитивы или наоборот, или вы хотите фильтровать классы другими способами, вызовите (пустую) вспомогательную функцию шаблона в вашей функции, которая определена только для классов, которые вы хотите принять.
Вы можете использовать (1) и (2) также в функциях-членах класса для принудительной проверки типов во всем классе.
Вы можете положить его в какой-нибудь умный макрос, чтобы облегчить боль.:)
Ну, вы можете создать свой шаблон, читая что-то вроде этого:
template<typename T>
class ObservableList {
std::list<T> contained_data;
};
Это, однако, сделает ограничение неявным, плюс вы не сможете просто предоставить что-либо, похожее на список. Существуют и другие способы ограничения используемых типов контейнеров, например, путем использования определенных типов итераторов, которые существуют не во всех контейнерах, но, опять же, это скорее неявное, чем явное ограничение.
Насколько мне известно, в текущем стандарте не существует конструкции, которая в полной мере отражала бы оператор Java оператора.
Есть способы ограничить типы, которые вы можете использовать в шаблоне, который вы пишете, с помощью определенных typedefs внутри вашего шаблона. Это гарантирует, что компиляция шаблона специализации для типа, который не включает этот конкретный typedef, завершится неудачей, так что вы можете выборочно поддерживать / не поддерживать определенные типы.
В C++11 введение концепций должно упростить это, но я не думаю, что оно будет делать именно то, что вы хотели бы.