Вот один из способов сделать это на основе принципа «Ошибка замещения - это не ошибка» (SFINAE). Это говорит о том, что при разрешении перегрузки, если компилятор C ++ пытается создать экземпляр шаблона и сталкивается с проблемой, он не вызывает ошибку компиляции. Вместо этого он просто удаляет этот конкретный шаблон из рассмотрения. Идея заключается в том, что если вы перегружаете функцию (т.е. имеете много разных функций с одинаковым именем, но разными параметрами), некоторые из которых являются шаблонами, а некоторые нет, вы никогда не получите ошибку компилятора, если один из Шаблонные функции-кандидаты не имеют смысла.
Эта конкретная техника может помочь вам двумя разными способами. Во-первых, давайте прямо сейчас предположим, что у меня есть черный ящик с именем «IsAdaptable», который может посмотреть на тип и сказать мне, является ли определенный тип адаптируемой функцией. Учитывая эту информацию, как мы можем заставить вашу функцию operator !
применяться только к типам, которые могут быть адаптированы? Ну, используя принцип SFINAE, нам нужно как-то сделать сигнатуру функции шаблона недействительной в случае, если тип ввода не адаптируется. Есть много способов сделать это, но один из них использует вспомогательный шаблон под названием «enable if». Вот полная реализация шаблона enable-if:
template <bool Condition, typename T> struct EnableIf {
typedef T type;
};
template <typename T> struct EnableIf<false, T> {
// Empty
};
Это странный шаблон. Он параметризован по двум аргументам - булевому условию и типу - таким образом, что если условие истинно, EnableIf экспортирует свой второй аргумент как type
, а если условие ложно, оно вообще ничего не экспортирует .
Утилита enable if template заключается в том, что она позволяет вам писать функции, которые доступны только в том случае, если определенные свойства соответствуют истинности аргумента шаблона. Предположим, например, что вы хотите написать такую функцию:
template <typename T>
ReturnType MyFunction(/* ... arguments ... */) {
/* ... body ... */
}
Однако вы хотите, чтобы эта функция могла использоваться только в том случае, если какой-то предикат «Предикат» выполняется для типа T. Затем вы можете изменить функцию так, чтобы она выглядела следующим образом:
template <typename T>
typename EnableIf<Predicate<T>::value, ActualReturnType>::type MyFunction(/* ... arguments ... */) {
/* ... body here ... */
}
Идея заключается в том, что если вы вызываете функцию MyFunction с некоторым параметром типа T, выполняется один из двух случаев. Во-первых, если Predicate<T>::value
истинно, то EnableIf
в итоге экспортирует вложенный тип с именем type
, который эквивалентен ActualReturnType
, и функция работает как обычно. С другой стороны, если Predicate<T>::value
равно false, то экземпляр EnableIf
не имеет вложенного в него типа type
. Компилятор обнаруживает это, и поскольку «ошибка замещения не является ошибкой», он снимает функцию с рассмотрения. Если есть другие возможные перегрузки, оставшиеся на рассмотрении, то компилятор выберет другую. Если нет, то он сообщает об ошибке, поскольку ни одна из возможных функций MyFunction
по-прежнему не работает.
В вашем случае идея состоит в том, чтобы написать operator !
так:
template <typename Pred>
typename EnableIf<IsAdaptable<Pred>::value, std::unary_negate<Pred> >::type
operator! (const Pred& p) {
return std::not1(p);
}
Здесь используется вышеприведенный трюк, чтобы сказать: «эта operator !
функция доступна только для типов, которые являются адаптируемыми функциями». Мы сейчас на полпути - учитывая реализацию IsAdaptable
, мы закончили.
Проблема в том, что написание IsAdaptable
совсем не просто. В конечном итоге он использует серию хаков, настолько ужасных, что заставит вас плакать. Но не бойся! Это не так сложно понять, когда вы увидите картину в целом.
Способ, которым мы можем заставить IsAdaptable
работать, - это использование SFINAE совершенно новым способом. Вот набросок идеи высокого уровня. Предположим, что у нас есть две функции, которые являются перегрузками другой, одна из которых возвращает тип с именем «Да», а другая возвращает тип с именем «Нет». Затем мы пишем эти функции так, чтобы версия «Да» всегда имела приоритет над версией «Нет», но версия «Да» доступна только в том случае, если некоторый данный тип является адаптируемым. В этом случае при вызове функции будет выполняться один из двух случаев:
- Данный тип может быть адаптирован. В этом случае доступны обе версии функции, но версия «Да» имеет приоритет над версией «Нет», и поэтому вызывается версия «Да».
- Тип, о котором идет речь, не адаптируется.В этом случае единственная доступная версия функции - это версия «Нет», и именно она называется.
Но как вы можете создавать такие функции?Оказывается, есть умное, но не особо сложное решение:
template <typename T> Yes TestFunction(typename T::argument_type* argument);
template <typename T> No TestFunction(...);
Обе эти функции называются TestFunction.Первый параметризован для некоторого типа T
и принимает в качестве аргумента указатель T::argument_type*
.Второй принимает параметр varargs.Теперь, если для некоторого типа T
мы пытаемся выполнить следующий вызов функции:
TestFunction<T>(NULL);
Тогда первая версия этой функции доступна, только если T
имеет тип с именем argument_type
, вложенный вЭто.Вторая версия всегда доступна.Однако из-за того, как работает разрешение перегрузки в C ++, функция varargs (функция, принимающая ...
) никогда не будет выбрана для какой-либо функции, более специфичной для ее списка аргументов.Следовательно, это вышеприведенное выражение имеет тип Yes
, если T
имеет argument_type
вложенное внутри него и имеет тип No
в противном случае.Мы почти на месте - если мы сможем каким-то образом определить, что является возвращаемым типом, у нас есть тест, чтобы увидеть, является ли T
адаптируемым!
Способ, которым мы сделаем этот последний шаг, этонемного окольный.Идея состоит в том, что мы определим типы Yes
и No
, чтобы они имели разные размеры:
typedef char Yes;
struct No {
char dummy[32];
};
Теперь мы знаем, что sizeof(Yes) == 1
и sizeof(No) > 1
.Объединение всего этого вместе дает нам окончательную версию IsAdaptable
:
template <typename T> struct IsAdaptable {
private:
typedef char Yes;
struct No {
char dummy[32];
};
template <typename U> static Yes test(typename U::argument_type*);
template <typename U> static No test(...);
public:
static const bool value = (sizeof(test<T>(0)) == sizeof(Yes));
};
Эта структура содержит все вышеперечисленные функции, а затем экспортирует 'true', если test<T>(0)
возвращает Yes
и false
иначе.Обратите внимание, что поскольку sizeof
на самом деле не оценивает его аргумент (он просто сообщает вам, сколько байтов он использует), нам фактически никогда не нужно реализовывать ни одну из этих функций.
Объединение абсолютно всего вместе дает окончательное решение:
template <bool cond, typename T> struct EnableIf {
typedef T type;
};
template <typename T> struct EnableIf<false, T> {
};
template <typename T> struct IsAdaptable {
private:
typedef char Yes;
struct No {
char buffer[32];
};
template <typename U> static Yes test(typename U::argument_type*);
template <typename U> static No test(...);
public:
static const bool result = (sizeof(test<T>(0)) == sizeof(Yes));
};
template<typename T>
inline typename EnableIf<IsAdaptable<T>::result, std::unary_negate<T> >::type operator !(const T& pred) {
return std::not1( pred );
}
Надеюсь, это поможет!