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

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

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

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

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

Ответ 1

Я предлагаю использовать функцию Boost static assert совместно с is_base_of из библиотеки типов Boost Type:

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.

[Незначительный EDIT 6/12/2013: использование объявленного, но не определенного шаблона приведет к компоновщику, а не компилятору, сообщения об ошибках.]

Ответ 2

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

Это нарушает множество концепций, которые люди ожидают на С++. Лучше использовать трюки, например, определять свои собственные черты. Например, возможно, observable_list хочет принять любой тип контейнера с параметрами typedefs const_iterator и begin и end, который возвращает 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...
};

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

Ответ 3

Простым решением, о котором никто еще не упомянул, является просто игнорировать проблему. Если я попытаюсь использовать int в качестве типа шаблона в шаблоне функции, который ожидает класс контейнера, такой как вектор или список, тогда я получу ошибку компиляции. Сырой и простой, но он решает проблему. Компилятор попытается использовать указанный вами тип, и если это не удается, он генерирует ошибку компиляции.

Единственная проблема заключается в том, что сообщения об ошибках, которые вы получаете, будут сложными для чтения. Тем не менее это очень распространенный способ сделать это. Стандартная библиотека заполнена шаблонами функций или классов, которые ожидают определенного поведения из типа шаблона и ничего не делают для проверки правильности используемых типов.

Если вам нужны более приятные сообщения об ошибках (или если вы хотите поймать случаи, которые бы не приводили к ошибке компилятора, но все же не имеют смысла), вы можете в зависимости от того, насколько сложным вы хотите это сделать, используйте либо Boost static assert или библиотеку Boost concept_check.

С обновленным компилятором у вас есть встроенный_n static_assert, который можно использовать вместо этого.

Ответ 4

Насколько я знаю, на С++ это невозможно. Тем не менее, есть планы добавить функцию, называемую "понятия" в новый стандарт С++ 0x, который обеспечивает функциональность, которую вы ищете. Эта статья Википедии о концепциях С++ объяснит это более подробно.

Я знаю, что это не устраняет вашу непосредственную проблему, но есть некоторые компиляторы С++, которые уже начали добавлять функции из нового стандарта, поэтому можно было бы найти компилятор, который уже реализовал функцию концепций.

Ответ 5

Мы можем использовать 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
}

Ответ 6

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

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

Ответ 7

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

Ответ

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

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

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

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

// 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(). Это тривиально в С++ с использованием неявных интерфейсов, но на самом деле это невозможно в Java с ограничениями типа.

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

Ответ 8

Я думаю, что все предыдущие ответы потеряли из виду лес для деревьев.

Генераторы Java не совпадают с шаблонами; они используют стирание типа, которое является динамической техникой, а не компиляцией временного полиморфизма, который является статическим методом. Должно быть очевидно, почему эти две очень разные тактики не хорошо заживают.

Вместо того, чтобы пытаться использовать конструкцию времени компиляции для имитации времени выполнения, давайте посмотрим, что на самом деле делает extends: в соответствии с переполнением стека и Wikipedia, extends используется для указания подкласса.

С++ также поддерживает подклассы.

Вы также показываете класс контейнера, который использует стирание типа в форме общего и расширяет для выполнения проверки типа. В С++ вы должны сами создавать стиральную машину типа, которая проста: сделать указатель на суперкласс.

Позвольте обернуть его в typedef, чтобы упростить его использование, а не сделать целый класс, et voila:

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 are not shapes

Теперь, кажется, List является интерфейсом, представляющим собой коллекцию. Интерфейс в С++ был бы просто абстрактным классом, то есть классом, который реализует только чистые виртуальные методы. Используя этот метод, вы можете легко реализовать свой Java-пример на С++ без каких-либо концепций или специализированных шаблонов. Он также будет работать так же медленно, как генерические стили Java-стиля из-за поиска виртуальных таблиц, но это часто может быть приемлемой потерей.

Ответ 9

Это невозможно в простом С++, но вы можете проверить параметры шаблона во время компиляции через Concept Checking, например. используя Boost BCCL.

Ответ 10

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, и компилятор будет расстроен во всех правильных местах.

Ответ 11

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

Нет.

В зависимости от того, что вы пытаетесь выполнить, могут быть адекватные (или даже лучше) замены.

Я просмотрел некоторый код STL (в Linux, я думаю, это тот, который происходит от реализации SGI). Он имеет "концептуальные утверждения"; например, если вам нужен тип, который понимает *x и ++x, утверждение концепции будет содержать этот код в функции do-nothing (или что-то подобное). Это требует некоторых накладных расходов, поэтому было бы разумно поместить его в макрос, определение которого зависит от #ifdef debug.

Если отношение подкласса действительно то, о чем вы хотите знать, вы могли бы утверждать в конструкторе, что T instanceof list (кроме того, что он написан "по-разному" на С++). Таким образом, вы можете проверить свой выход из компилятора, не имея возможности проверить его для вас.

Ответ 12

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

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

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

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

Ответ 13

Ну, вы можете создать свой шаблон, читающий что-то вроде этого:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

Это, однако, сделает ограничение неявным, плюс вы не можете просто предоставить что-либо похожее на список. Существуют и другие способы ограничить используемые типы контейнеров, например, используя конкретные типы итераторов, которые не существуют во всех контейнерах, но опять же это более неявное, чем явное ограничение.

Насколько я знаю, конструктор, который бы полностью отразил оператор оператора Java, в существующем стандарте не существует.

Есть способы ограничить типы, которые вы можете использовать внутри шаблона, который вы пишете, используя определенные typedef внутри вашего шаблона. Это гарантирует, что компиляция специализации шаблона для типа, который не включает этот конкретный typedef, не будет работать, поэтому вы можете выборочно поддерживать/не поддерживать определенные типы.

В С++ 11 введение понятий должно сделать это проще, но я не думаю, что он сделает именно то, что вы хотите.