Ограничения на параметр шаблона

Рейтинг: 3Ответов: 6Опубликовано: 18.04.2015

Есть шаблонный класс массива

template<class _Ty> class Array;

в котором реализована функция сортировки, которая требует наличия у параметра шаблона функции CompareTo(_Ty& other); Так вот, как указать в определении шаблона что у параметра _Ty обязательно будет функция ComareTo(_Ty& other). Что то подобное реализовано в C#, но не знаю как и возможно ли такое проделать на плюсах?

Ответы

▲ 8Принят

Вот мой вариант:

#include <type_traits>

template <typename T>
class has_CompareTo_self
{
    typedef char one[1];
    typedef char two[2];

    template <typename F, F> struct type_check;

    template <typename C> static one& test( type_check<int (C::*)(C&), &C::CompareTo>* );
    template <typename> static two& test(...);

public:
    enum { value = sizeof(test<T>(0)) == sizeof(one) };
};

template<class _Ty> class Array
{
    static_assert(has_CompareTo_self<_Ty>::value, "no appropriate CompareTo found");
};

Преимущество этого варианта в том, что он проверяет сигнатуры. То есть, принимает такой класс:

class Good
{
public:
    int CompareTo(Good& other);
};

но не такой:

class Bad
{
public:
    long CompareTo(Bad& other);
};

Проверка: http://ideone.com/drYcz4


Давайте попробую объяснить, как эта шаблонная магия работает, чтобы мой вклад был больше, чем простая адаптация этого ответа.

Главной идеей всех таких штук является принцип substitution failure is not an error (SFINAE): если при разворачивании шаблона получается несколько перегрузок функции, но одна из них не компилируется, то это не ошибка, а просто молча игнорируется.

Сначала простое: one и two — две структуры данных гарантированно разного размера. Это нам понадобится в дальнейшем. Больше от этих структур ничего не требуется.

Теперь, центральный компонент кода — объявление

template <typename C> static one& test( type_check<int (C::*)(C&), &C::CompareTo>* );

Оно скомпилируется лишь если type_check<int (C::*)(C&), &C::CompareTo> имеет смысл. Для этого у класса C как минимум должен быть метод CompareTo.

Затем, у нас есть хитрый шаблон type_check, принимающий аргументами тип F, и константное выражение типа F! То есть для того, чтобы упоминание этого шаблона скомпилировалось, нужно, чтобы выражение, переданное ему вторым аргументом, имело такой тип, как в первом аргументе. В нашем случае это накладывает требование: &C::CompareTo должна иметь тип int (C::*)(C&) (то есть, функция-член, принимающая аргумент типа C& и возвращающая int).

Итак, это самое центральное объявление скомпилируется тогда и только тогда, когда у класса C есть функция-член int C::CompareTo(C&) — то, что нам и надо!

Как это работает дальше? У нас есть ещё одна перегрузка:

template <typename> static two& test(...);

которая компилируется всегда.

Посмотрим на строку

enum { value = sizeof(test<T>(0)) == sizeof(one) };

Она объявляет внутренний enum с одним значением value. Правую часть (то есть, значение самого value) компилятор может вычислить, т. к. это константное выражение времени компиляции! Каков тип test<T>(0)? Если центральная строка скомпилировалась, то 0 подходит в качестве указателя на структуру type_check<...>, и тип возвращаемого значения — one. Если нет, то работает вторая перегрузка, и тип возвращаемого значения — two. Различить эти два случая помогает сравнение размеров: sizeof(one) не равен sizeof(two). Таким образом, в зависимости от компилируемости центральной строки значение valuetrue или false.

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

Дальше всё просто: мы применяем static_assert в теле нашего шаблона Array. Конец!


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

▲ 3

В C++11 можно сделать static_assert с std::is_base_of. Придется сделать базовый класс с методом ComareTo(_Ty& other); и проверять, является ли этот класс базовым классом для текущей спецификации шаблона.

▲ 2

В определении шаблона - пока никак. В настоящий момент такой функциональности просто не существует. Она прорабатывается на уровне комитета, но не вошла в стандарт, пока.

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

▲ 2

Установить ограничение на параметр шаблона просто так (из вредности) нельзя. Но зато можно проверить наличие у параметра шаблона определённой функции и в случае отсутствия таковой переключиться на отдельную специализацию шаблона.

#include <iostream>
using namespace std;

template <typename T>
class ensure_CompareTo {
    // используем SFINAE, чтобы определить наличие функции
    // когда функция есть, выбирается первый шаблон, когда нет - второй
    template <typename U> static T test(char (*)[sizeof(&U::CompareTo)]);
    template <typename U> static void test(...);
public:
    // этот алиас зависит от того, какой шаблон был выбран с помощью SFINAE
    // он равен либо исходному типу, либо void
    using type = decltype(test<T>(0));
};

// базовый шаблон массива
// второй параметр выводится автоматически из первого
// он либо совпадает с ним, когда требуемая функция есть, либо равен void
template <typename T, typename TT = typename ensure_CompareTo<T>::type>
class Array {
public:
    static const char* desc() { return "отсортированный массив"; }
};

// специализация шаблона массива для случая, когда второй параметр, выводимый
// автоматически из первого, оказался равным void, т.е. когда требуемой
// функции в параметре шаблона не оказалось
template <typename T>
class Array<T, void> {
public:
    static const char* desc() { return "просто массив"; }
};

class A {
public:
    int CompareTo(A& other);
};

class B { };

int main()
{
    cout << Array<A>::desc() << endl;
    cout << Array<B>::desc() << endl;

    return 0;
}
▲ 1

Не уверен, что это возможно. Но почему обязательно привязывать интерфейс сравнения к шаблону класса? Почему бы не сделать это как в Java или C#? Там есть параметризованный интерфейс Comparable (IComparable для C#), который реализуют классы, которые должны иметь возможность сравнения с экземплярами других классов. Таким образом вы получите даже большую гибкость, обеспечив возможность сравнения одновременно с несколькими типами данных:

template <class T> class Comparable {
public:
    virtual bool CompareTo(T* other) = 0;
};

template <class T> class Array : Comparable<Array<T> >, Comparable<int> {
public:
    Array() {}

    bool CompareTo(Array<T>* other) {
        return false;
    }

    bool CompareTo(int* other) {
        return false;
    }
};

int main() {
    Array<int> a;
    Array<int> b;
    int c = 123;
    a.CompareTo(&b);
    a.CompareTo(&c);
    return 0;
}
▲ 1

Если мы хотим просто проверить наличие члена - достаточно его вызвать.

Мы не хотим выполнять этот код, соответственно мы будем вызывать его в контексте который не приводит к выполнению кода, а именно sizeof, decltype или noexcept.

Код, который будет выполняться, во всех случаях выглядит одинаково - создание объекта с помощью std::declval<T>() и вызов метода:

std::declval<T>().CompareTo(std::declval<T&>())

Если надо проверить несколько свойств, можно использовать оператор "запятая":

std::declval<T>().f(), std::declval<T>().g()

При использовании decltype мы получаем тип, и его можно использовать как дополнительный параметр шаблона:

template<class T,
         class=decltype(std::declval<T>().CompareTo(std::declval<T&>()))>
struct Array {};

Этому decltype можно сделать псевдоним (alias)

template<class T> using has_CompareTo = decltype(std::declval<T>().CompareTo(std::declval<T&>()));
template<class T, class=has_CompareTo<T>> struct Array {};

Для sizeof и noexcept мы тоже можем использовать параметр шаблона:

template<class T, int=sizeof(std::declval<T>().CompareTo(std::declval<T&>()), 0)>
struct Array {};

, 0 на конце выражения нужно на случай если результат CompareTo имеет тип void.

Также мы можем написать это как член класса:

template<class T>
struct Array {
    enum { has_CompareTo = noexcept(std::declval<T>().CompareTo(std::declval<T&>()), 0) };
    // или
    using has_CompareTo = decltype(std::declval<T>().CompareTo(std::declval<T&>()));
};

Вариант с параметром шаблона интересен тем что он включает SFINAE, но в данном случае оно не нужно.