Ответ 1
Да, концепции lite в основном одевают SFINAE. Плюс это позволяет более глубокую интроспекцию, чтобы обеспечить лучшую перегрузку. Однако это работает только в том случае, если предикаты понятия определены как concept bool
. Улучшенная перегрузка не работает с текущими предикатами концепции, но может быть использована условная перегрузка. Давайте посмотрим, как мы можем определять предикаты, ограничивать шаблоны и функции перегрузки в С++ 14. Это довольно долго, но в нем рассказывается, как создать все инструменты, необходимые для выполнения этого в С++ 14.
Определение предикатов
Во-первых, довольно уродливо читать предикат со всеми std::declval
и decltype
всюду. Вместо этого мы можем воспользоваться тем фактом, что мы можем ограничить функцию, используя трейлинг-тип (из сообщения Eric Nieblers blog здесь), вот так:
struct Incrementable
{
template<class T>
auto requires_(T&& x) -> decltype(++x);
};
Итак, если ++x
недействительно, то функция-член requires_
не может быть вызвана. Таким образом, мы можем создать признак models
, который просто проверяет, может ли requires_
быть вызванным, используя void_t
:
template<class Concept, class Enable=void>
struct models
: std::false_type
{};
template<class Concept, class... Ts>
struct models<Concept(Ts...), void_t<
decltype(std::declval<Concept>().requires_(std::declval<Ts>()...))
>>
: std::true_type
{};
Ограничивающие шаблоны
Поэтому, когда мы хотим ограничить шаблон на основе концепции, нам все равно придется использовать enable_if
, но мы можем использовать этот макрос, чтобы сделать его более чистым:
#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0
Таким образом, мы можем определить функцию increment
, которая ограничена на основе концепции Incrementable
:
template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
++x;
}
Итак, если мы назовем increment
тем, что не является Incrementable
, мы получим такую ошибку:
test.cpp:23:5: error: no matching function for call to 'incrementable'
incrementable(f);
^~~~~~~~~~~~~
test.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo]
template<class T, REQUIRES(models<Incrementable(T)>())>
^
Функции перегрузки
Теперь, если мы хотим перегрузить, мы хотим использовать условную перегрузку. Предположим, что мы хотим создать std::advance
с использованием предикатов преемственности, мы могли бы определить его так (на данный момент мы будем игнорировать уменьшающийся случай):
struct Incrementable
{
template<class T>
auto requires_(T&& x) -> decltype(++x);
};
struct Advanceable
{
template<class T, class I>
auto requires_(T&& x, I&& i) -> decltype(x += i);
};
template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
void advance(Iterator& it, int n)
{
it += n;
}
template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
void advance(Iterator& it, int n)
{
while (n--) ++it;
}
Однако это вызывает неоднозначную перегрузку (в понятиях lite это все равно будет неоднозначной перегрузкой, если мы не изменим наши предикаты, чтобы ссылаться на другие предикаты в concept bool
), когда она используется с итератором std::vector
. Мы хотим сделать заказ, который мы можем сделать, используя условную перегрузку. Можно подумать о написании чего-то вроде этого (что недопустимо С++):
template<class Iterator>
void advance(Iterator& it, int n) if (models<Advanceable(Iterator, int)>())
{
it += n;
}
else if (models<Incrementable(Iterator)>())
{
while (n--) ++it;
}
Итак, если первая функция не вызывается, она вызовет следующую функцию. Поэтому давайте начнем с реализации его для двух функций. Мы создадим класс под названием basic_conditional
, который принимает два функциональных объекта в качестве параметров шаблона:
struct Callable
{
template<class F, class... Ts>
auto requires_(F&& f, Ts&&... xs) -> decltype(
f(std::forward<Ts>(xs)...)
);
};
template<class F1, class F2>
struct basic_conditional
{
// We don't need to use a requires clause here because the trailing
// `decltype` will constrain the template for us.
template<class... Ts>
auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...))
{
return F1()(std::forward<Ts>(xs)...);
}
// Here we add a requires clause to make this function callable only if
// `F1` is not callable.
template<class... Ts, REQUIRES(!models<Callable(F1, Ts&&...)>())>
auto operator()(Ts&&... xs) -> decltype(F2()(std::forward<Ts>(xs)...))
{
return F2()(std::forward<Ts>(xs)...);
}
};
Итак, теперь это означает, что нам нужно определить наши функции как объекты объектов:
struct advance_advanceable
{
template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
void operator()(Iterator& it, int n) const
{
it += n;
}
};
struct advance_incrementable
{
template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
void operator()(Iterator& it, int n) const
{
while (n--) ++it;
}
};
static conditional<advance_advanceable, advance_incrementable> advance = {};
Итак, если мы попытаемся использовать его с std::vector
:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
auto iterator = v.begin();
advance(iterator, 4);
std::cout << *iterator << std::endl;
Он будет компилировать и распечатывать 5
.
Однако std::advance
фактически имеет три перегрузки, поэтому мы можем использовать basic_conditional
для реализации conditional
, который работает для любого количества функций с использованием рекурсии:
template<class F, class... Fs>
struct conditional : basic_conditional<F, conditional<Fs...>>
{};
template<class F>
struct conditional<F> : F
{};
Итак, теперь мы можем написать полный std::advance
следующим образом:
struct Incrementable
{
template<class T>
auto requires_(T&& x) -> decltype(++x);
};
struct Decrementable
{
template<class T>
auto requires_(T&& x) -> decltype(--x);
};
struct Advanceable
{
template<class T, class I>
auto requires_(T&& x, I&& i) -> decltype(x += i);
};
struct advance_advanceable
{
template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())>
void operator()(Iterator& it, int n) const
{
it += n;
}
};
struct advance_decrementable
{
template<class Iterator, REQUIRES(models<Decrementable(Iterator)>())>
void operator()(Iterator& it, int n) const
{
if (n > 0) while (n--) ++it;
else
{
n *= -1;
while (n--) --it;
}
}
};
struct advance_incrementable
{
template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())>
void operator()(Iterator& it, int n) const
{
while (n--) ++it;
}
};
static conditional<advance_advanceable, advance_decrementable, advance_incrementable> advance = {};
Перегрузка с помощью Lambdas
Однако, кроме того, мы могли бы использовать lambdas для записи вместо объектов функций, которые могут помочь сделать его более чистым для записи. Поэтому мы используем макрос STATIC_LAMBDA
для создания lambdas во время компиляции:
struct wrapper_factor
{
template<class F>
constexpr wrapper<F> operator += (F*)
{
return {};
}
};
struct addr_add
{
template<class T>
friend typename std::remove_reference<T>::type *operator+(addr_add, T &&t)
{
return &t;
}
};
#define STATIC_LAMBDA wrapper_factor() += true ? nullptr : addr_add() + []
И добавьте функцию make_conditional
, которая constexpr
:
template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
return {};
}
Затем мы можем теперь написать функцию advance
следующим образом:
constexpr const advance = make_conditional(
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Advanceable(decltype(it), int)>()))
{
it += n;
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Decrementable(decltype(it))>()))
{
if (n > 0) while (n--) ++it;
else
{
n *= -1;
while (n--) --it;
}
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Incrementable(decltype(it))>()))
{
while (n--) ++it;
}
);
Это немного компактнее и удобочитаемо, чем использование версий функциональных объектов.
Кроме того, мы могли бы определить функцию modeled
, чтобы уменьшить decltype
уродство:
template<class Concept, class... Ts>
constexpr auto modeled(Ts&&...)
{
return models<Concept(Ts...)>();
}
constexpr const advance = make_conditional(
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Advanceable>(it, n)))
{
it += n;
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Decrementable>(it)))
{
if (n > 0) while (n--) ++it;
else
{
n *= -1;
while (n--) --it;
}
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Incrementable>(it)))
{
while (n--) ++it;
}
);
Наконец, если вы заинтересованы в использовании существующих библиотечных решений (вместо того, чтобы кататься самостоятельно, как я показал). Существует библиотека Tick, которая обеспечивает структуру для определения понятий и ограничения шаблонов. И библиотека Fit может обрабатывать функции и перегружать.