Шаблони C ++, які приймають лише певні типи


158

У Java ви можете визначити загальний клас, який приймає лише типи, що розширюють клас на ваш вибір, наприклад:

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

Це робиться за допомогою ключового слова "розширює".

Чи є якийсь простий еквівалент цього ключового слова в C ++?


вже досить давнє запитання ... Я відчуваю, що тут не вистачає (також у відповідях), що дженеріки Java насправді не є еквівалентом шаблонів на C ++. Є подібності, але imho слід бути обережним, безпосередньо переводячи рішення Java на C ++, просто зрозуміти, що вони можуть бути зроблені для різних видів проблем;)
idclev 463035818

Відповіді:


104

Я пропоную використовувати функцію статичного затвердженняis_base_of Boost разом з бібліотекою ознак типу Boost:

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: Використання оголошеного, але не визначеного шаблону призведе до появи посилань , а не компілятора, повідомлень про помилки.]


Статичні твердження також приємні. :)
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
@Andreyua: Я не дуже розумію, чого не вистачає. Ви можете спробувати оголосити змінну 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та функцію a 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...
};

На прикладі вище представлено багато концепцій, які демонструють особливості C ++ 11. Деякі пошукові терміни для цікавих - це різноманітні шаблони, SFINAE, вирази SFINAE та риси типу.


2
Я ніколи не розумів, що до цього часу шаблони C ++ використовують введення качок. Вигляд химерного!
Енді

2
Враховуючи великі політичні обмеження C ++, введені в C , не впевнені, чому template<class T:list>таке кривдне поняття. Дякую за пораду.
bvj

60

Просте рішення, про яке ще ніхто не згадував, - це просто ігнорувати проблему. Якщо я спробую використовувати intяк тип шаблону в шаблоні функції, який очікує клас контейнера, такий як вектор або список, то я отримаю помилку компіляції. Сира та проста, але вона вирішує проблему. Компілятор спробує використати вказаний тип, і якщо це не вдасться, він генерує помилку компіляції.

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

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

З сучасним компілятором у вас є вбудований_in static_assert, який можна використовувати замість цього.


7
Так, я завжди вважав, що шаблони є найбільш близькою справою для набору качок у C ++. Якщо в ньому є всі елементи, необхідні для шаблону, його можна використовувати в шаблоні.

@John: Вибачте, я не можу зробити це головою чи хвостами. Якого типу це Tі звідки цей код називається? Без якогось контексту я не маю шансів зрозуміти цей фрагмент коду. Але те, що я сказав, є правдою. Якщо ви спробуєте зателефонувати toString()на тип, який не має toStringфункції члена, ви отримаєте помилку компіляції.
jalf

@John: наступного разу, можливо, ви повинні бути трохи менш сприятливими, що зволікають людей, коли проблема у вашому коді
jalf

@jalf, добре. +1. Це була чудова відповідь, просто намагаючись зробити це найкращим. Вибачте за непрочитаність. Я думав, що ми говорили про використання типу в якості параметра для класів, а не для шаблонів функцій, які, напевно, є членами колишнього, але потрібно викликати компілятор для позначення.
Іван

13

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

13

Наскільки я знаю, це наразі неможливо на C ++. Однак планується додати функцію під назвою "поняття" в новий стандарт C ++ 0x, який забезпечує функціональні можливості, які ви шукаєте. Ця стаття у Вікіпедії про поняття C ++ пояснить це більш докладно.

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


4
Концепції, на жаль, відпали від стандартних.
macbirdie

4
Для C ++ 20 слід прийняти обмеження та концепції.
Петро Яворик

Це можливо навіть без понять, використовуючи static_assertта SFINAE, як показують інші відповіді. Залишається проблемою для когось із Java або C #, або Haskell (...) полягає в тому, що компілятор C ++ 20 не робить перевірку визначення відповідності необхідним поняттям, що роблять Java і C #.
user7610

10

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

Java-дженерики - це не те саме, що шаблони ; вони використовують стирання типу , що є динамічною технікою , а не компілювання часового поліморфізму , що є статичною технікою . Повинно бути очевидним, чому ці дві дуже різні тактики не гелюються добре.

Замість того, щоб намагатися використовувати конструкцію часу компіляції для імітації часу виконання, давайте розглянемо, що extendsнасправді робить: згідно із переповненням стека та Вікіпедії , розширення використовується для позначення підкласифікації.

C ++ також підтримує підкласифікацію.

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

Давайте загорнумо його в typedef, щоб полегшити його використання, а не складати цілий клас та ін. 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's are not shapes

Тепер, схоже, Список - це інтерфейс, що представляє собою якусь колекцію. Інтерфейс в C ++ був би просто абстрактним класом, тобто класом, який не реалізує нічого, крім чистого віртуального методу. Використовуючи цей метод, ви можете легко реалізувати свій приклад Java в C ++, без будь-яких концепцій чи спеціалізацій шаблонів. Він також працюватиме так само повільно, як і дженерики стилів Java через вигляд віртуальних таблиць, але це часто може бути прийнятною втратою.


3
Я не прихильник відповідей, які використовують фрази на зразок "це повинно бути очевидно" або "всі знають", а потім продовжую пояснювати те, що очевидно чи загальновідомо. Очевидним є відношення до контексту, досвіду та контексту досвіду. Такі твердження по суті є грубими.
3Dave

2
@DavidLively Вже два роки пізно критикувати цю відповідь за етикет, але я також не згоден з вами в цьому конкретному випадку; Я пояснив, чому вони дві техніки не йдуть разом до того, як заявили, що це очевидно, а не після. Я надав контекст, а потім сказав, що висновок з цього контексту був очевидним. Це не точно відповідає вашій формі.
Аліса

Автор цієї відповіді сказав, що щось було очевидним після важкого підйому. Я не думаю, що автор мав намір сказати, що рішення було очевидним.
Люк Гехорсам

10

Так виглядає еквівалент, який приймає лише типи T, отримані з типу List

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

8

Резюме: Не робіть цього.

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

Обмеження типу Java - це помилка, а не функція. Вони є там, тому що Java виконує стирання даних на генеріках, тому Java не може зрозуміти, як викликати методи на основі значення параметрів типу.

З іншого боку, C ++ не має такого обмеження. Типи параметрів шаблону можуть бути будь-якого типу, сумісного з операціями, з якими вони використовуються. Не повинно бути загального базового класу. Це схоже на "Дайпінг качок" Пітона, але зроблено під час компіляції.

Простий приклад, що показує силу шаблонів:

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


2
Якщо ви пропонуєте ніколи не спеціалізовувати шаблони, чи можете ви також пояснити, чому це є мовою?

1
Я розумію, але якщо ваш аргумент шаблону повинен бути похідний від конкретного типу, то краще мати просте інтерпретацію повідомлення з static_assert, ніж звичайна помилка компілятора блювота.
jhoffman0x

1
Так, C ++ тут ​​більш виразний, але хоча це взагалі гарна річ (оскільки ми можемо виражати більше з меншим), іноді ми хочемо свідомо обмежувати владу, яку ми надаємо, щоб отримати впевненість у тому, що ми повністю розуміємо систему.
j_random_hacker

Спеціалізація типу @Curg корисна, коли ви хочете скористатися якоюсь справою, яка може бути зроблена лише для певних типів. наприклад, булевим є ~ нормально ~ один байт кожен, навіть якщо один байт може ~ нормально ~ вміщувати 8 біт / булеві; клас колекції шаблонів може (і у випадку std :: map робить) спеціалізуватися на булевий, щоб він міг упакувати дані більш щільно для збереження пам'яті.
thecoshman

Крім того, для уточнення, ця відповідь не скаже «ніколи не спеціалізуй шаблони», це говорить, що не використовуй цю функцію, щоб намагатися обмежити, які типи можна використовувати з шаблоном.
thecoshman

6

Це не можливо в звичайному C ++, але ви можете перевірити параметри шаблону під час компіляції за допомогою перевірки концепції, наприклад, використовуючи BCCL Boost .

Станом на C ++ 20, концепції стають офіційною ознакою мови.


2
Ну, це можливо, але перевірка концепції по - , як і раніше є хорошою ідеєю. :)
j_random_hacker

Я насправді мав на увазі, що в "простому" С ++ це неможливо. ;)
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 введення понять повинно полегшити це, але я не думаю, що це зробить саме те, що ви хотіли б.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.