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

159

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

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

Это делается с помощью ключевого слова "extends".

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

mgamer
источник
уже довольно старый вопрос ... Я чувствую, что здесь не хватает (также из ответов), что дженерики Java на самом деле не являются эквивалентом шаблонов в C ++. Есть сходства, но имхо нужно быть осторожным с прямым переводом Java-решения на C ++, просто чтобы понять, что они, возможно, созданы для разных задач;)
idclev 463035818

Ответы:

104

Я предлагаю использовать функцию статического подтверждения 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: использование объявленного, но не определенного шаблона приведет к сообщениям об ошибках компоновщика , а не компилятора.]

j_random_hacker
источник
Статические утверждения также хороши. :)
macbirdie
5
@John: я боюсь, что специализация будет соответствовать только myBaseTypeточно. Перед тем как отказаться от Boost, вы должны знать, что большая часть кода является шаблоном кода только для заголовков, поэтому во время выполнения не требуется никаких затрат памяти или времени для вещей, которые вы не используете. Также конкретные вещи, которые вы будете использовать здесь ( BOOST_STATIC_ASSERT()и is_base_of<>), могут быть реализованы с использованием только объявлений (то есть без фактических определений функций или переменных), поэтому они не будут занимать ни места, ни времени.
j_random_hacker
50
C ++ 11 пришел. Теперь мы можем использовать static_assert(std::is_base_of<List, T>::value, "T must extend list").
Сиюань Рен
2
Кстати, причина, по которой двойные скобки необходимы, заключается в том, что BOOST_STATIC_ASSERT является макросом, а лишние скобки не позволяют препроцессору интерпретировать запятую в аргументах функции is_base_of как второй аргумент макроса.
jfritz42
1
@ Андреа: Я не очень понимаю, чего не хватает. Вы можете попробовать объявить переменную my_template<int> x;или my_template<float**> y;и убедиться, что компилятор это разрешает, а затем объявить переменную my_template<char> z;и убедиться, что это не так.
j_random_hacker
134

Как правило, это неоправданно в 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и функцию- член beginand, 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 и черты типа.

Rapptz
источник
2
Я никогда не понимал, что шаблоны C ++ используют утечную типизацию до сегодняшнего дня. Вид странного!
Энди
2
Учитывая обширные ограничения политики C ++, введенные в C , не уверен, почему template<class T:list>такая оскорбительная концепция. Спасибо за чаевые.
Bvj
61

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

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

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

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

jalf
источник
7
Да, я всегда думал, что шаблоны - это самая близкая вещь к печатанию на уток в C ++. Если в нем есть все элементы, необходимые для шаблона, его можно использовать в шаблоне.
@John: извините, я не могу сделать голову или хвосты этого. Какой тип Tи откуда этот код называется? Без некоторого контекста у меня нет шансов понять этот фрагмент кода. Но то, что я сказал, правда. Если вы попытаетесь вызвать toString()тип, который не имеет toStringфункции-члена, вы получите ошибку компиляции.
jalf
@John: в следующий раз, возможно, вам следует быть менее склонными к триггерам, когда люди с недоумением голосуют, когда проблема в вашем коде
jalf 12.12.11
@ jalf, хорошо. +1. Это был отличный ответ, просто пытаясь сделать его лучшим. Извините за неправильное прочтение. Я думал, что мы говорим об использовании типа в качестве параметра для классов, а не для шаблонов функций, которые, как я полагаю, являются членами первых, но для вызова компилятора необходимо вызвать их.
Джон
13

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

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

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

Барри Карр
источник
4
Понятия были исключены из стандарта, к сожалению.
Macbirdie
4
Ограничения и концепции должны быть приняты для C ++ 20.
Петр Яворик
Это возможно даже без понятий, используя static_assertи SFINAE, как показывают другие ответы. Оставшаяся проблема для кого-то из Java, C # или Haskell (...) заключается в том, что компилятор C ++ 20 не выполняет проверку определений в соответствии с необходимыми понятиями, как в Java и C #.
user7610
10

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

Обобщения 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, из-за поиска в виртуальной таблице, но это часто может быть приемлемой потерей.

Алиса
источник
3
Я не фанат ответов, которые используют такие фразы, как «это должно быть очевидно» или «все знают», а затем продолжаю объяснять, что является очевидным или общеизвестным. Очевидное относится к контексту, опыту и контексту опыта. Такие заявления по своей сути грубы.
3Dave
2
@DavidLively Уже слишком поздно, чтобы критиковать этот ответ за этикет, но я также не согласен с вами в этом конкретном случае; Я объяснил, почему они не работают вместе, прежде чем утверждать, что это очевидно, а не после. Я предоставил контекст, а затем сказал, что вывод из этого контекста очевиден. Это не совсем соответствует вашей форме.
Алиса
Автор этого ответа сказал, что что-то было очевидно после некоторой тяжелой работы. Я не думаю, что автор хотел сказать, что решение было очевидным.
Люк Gehorsam
10

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

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};
nh_
источник
8

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

Ответ 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 ++ не имеют явных ограничений типов, они по-прежнему безопасны для типов и не будут компилироваться с кодом, который не поддерживает правильные операции.

catphive
источник
2
Если вы предлагаете никогда не специализировать шаблоны, можете ли вы объяснить, почему это на языке?
1
Я понимаю вашу точку зрения, но если ваш шаблонный аргумент должен быть получен из определенного типа, то лучше иметь простое для интерпретации сообщение из static_assert, чем обычную рвоту с ошибкой компилятора.
jhoffman0x
1
Да, C ++ здесь более выразителен, но, хотя в целом это хорошо (потому что мы можем выразить больше с меньшими затратами), иногда мы хотим сознательно ограничивать силу, которую мы даем себе, чтобы получить уверенность в том, что мы полностью понимаем систему.
j_random_hacker
@Curg тип специализации полезен, когда вы хотите иметь возможность воспользоваться тем, что может быть сделано только для определенных типов. например, логическое значение ~ обычно ~ один байт каждый, хотя один байт может ~ обычно ~ содержать 8 бит / логическое значение; класс коллекции шаблонов может (и в случае с std :: map делает) специализироваться на булевых значениях, поэтому он может более плотно упаковать данные для экономии памяти.
Thecoshman
Также, чтобы уточнить, этот ответ не говорит «никогда не специализировать шаблоны», он говорит, что не используйте эту функцию, чтобы попытаться ограничить типы, которые можно использовать с шаблоном.
Thecoshman
6

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

Начиная с C ++ 20 концепции становятся официальной чертой языка.

macbirdie
источник
2
Ну, это возможно, но проверка концепции по - прежнему является хорошей идеей. :)
j_random_hacker
Я на самом деле имел в виду, что это было невозможно в «простом» C ++. ;)
Macbirdie
5
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, и компилятор расстроится во всех нужных местах.

Стюарт
источник
@Zehelvion Type::FooSecurityиспользуется в шаблонном классе. Если класс, переданный в аргументе шаблона, не имеет FooSecurity, попытка его использования вызывает ошибку. Он уверен, что если класс, переданный в аргументе шаблона, не имеет FooSecurity, он не является производным от Base.
GingerPlusPlus
2

Использование концепции 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, поэтому не уверен, что происходит.

Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1

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

Нет.

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

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

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

Йонас Кёлкер
источник
1

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

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

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

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

Яап
источник
-2

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

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

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

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

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

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

Тимо Гойш
источник