При наличии типа с конструктором шаблона с переменным числом аргументов, который передает аргументы классу реализации, возможно ли ограничить типы, передаваемые с помощью SFINAE?
Сначала рассмотрим невариантный случай, когда конструктор использует универсальную ссылку. Здесь можно отключить пересылку неконстантной lvalue-ссылки через SFINAE, чтобы использовать вместо этого конструктор копирования.
struct foo
{
foo() = default;
foo(foo const&)
{
std::cout << "copy" << std::endl;
}
template <
typename T,
typename Dummy = typename std::enable_if<
!std::is_same<
T,
typename std::add_lvalue_reference<foo>::type
>::value
>::type
>
foo(T&& x)
: impl(std::forward<T>(x))
{
std::cout << "uref" << std::endl;
}
foo_impl impl;
};
Это ограничение универсальной ссылки полезно, потому что в противном случае класс реализации получил бы неконстантную ссылку lvalue типа foo
о которой он не знает.
Полный пример на LWS.
Но как это работает с переменными шаблонами? Это вообще возможно? Если так, то как? Наивное расширение не работает:
template <
typename... Args,
typename Dummy = typename std::enable_if<
!std::is_same<
Args...,
typename std::add_lvalue_reference<foo>::type
>::value
>::type
>
foo(Args&&... args)
: impl(std::forward<Args>(args)...)
{
std::cout << "uref" << std::endl;
}
(Также на LWS.)
РЕДАКТИРОВАТЬ: Я обнаружил, что Р. Мартиньо Фернандес написал в блоге об этой проблеме в 2012 году: http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html
Вот различные способы написания правильно ограниченного шаблона конструктора, в порядке возрастания сложности и соответствующего возрастающего порядка богатства функций и убывающего порядка числа ошибок.
Эта конкретная форма EnableIf будет использоваться, но это деталь реализации, которая не меняет сути описанных здесь методов. Также предполагается, что есть And
а также Not
псевдонимы для объединения различных мета-вычислений. Например. And<std::is_integral<T>, Not<is_const<T>>>
удобнее чем std::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>
,
Я не рекомендую какую-либо конкретную стратегию, потому что любой ограничение гораздо лучше, чем отсутствие ограничений вообще, когда дело доходит до шаблонов конструктора. Если возможно, избегайте первых двух методов, которые имеют очень очевидные недостатки — остальные являются разработками на ту же тему.
template<typename T>
using Unqualified = typename std::remove_cv<
typename std::remove_reference<T>::type
>::type;
struct foo {
template<
typename... Args
, EnableIf<
Not<std::is_same<foo, Unqualified<Args>>...>
>...
>
foo(Args&&... args);
};
Выгода: избегает участия конструктора в разрешении перегрузки в следующем сценарии:
foo f;
foo g = f; // typical copy constructor taking foo const& is not preferred!
недостаток: участвует в каждый другой вид разрешения перегрузки
Поскольку конструктор имеет моральные последствия построения foo_impl
от Args
кажется естественным выразить ограничения на эти точные термины:
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
foo(Args&&... args);
Выгода: Теперь это официально ограниченный шаблон, так как он участвует в разрешении перегрузки, только если выполняется некоторое семантическое условие.
Минус: Является ли следующее действительным?
// function declaration
void fun(foo f);
fun(42);
Если, например, foo_impl
является std::vector<double>
тогда да, код действителен. Так как std::vector<double> v(42);
это правильный способ сооружать вектор такого типа, то это справедливо для перерабатывать от int
в foo
, Другими словами, std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value
оставляя в стороне дело других конструкторов для foo
(обратите внимание на поменялся порядок параметров — это печально).
Естественно, сразу приходит на ум следующее:
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
explicit foo(Args&&... args);
Вторая попытка, которая отмечает конструктор explicit
,
Выгода: Избегает вышеуказанного недостатка! И это тоже не займет много времени — пока вы этого не забудете explicit
,
Недостатки: Если foo_impl
является std::string
, тогда следующее может быть неудобно:
void fun(foo f);
// No:
// fun("hello");
fun(foo { "hello" });
Это зависит от того, foo
например, предназначен для тонкой обертки вокруг foo_impl
, Вот то, что я считаю более раздражающим недостатком, если предположить, foo_impl
является std::pair<int, double*>
,
foo make_foo()
{
// No:
// return { 42, nullptr };
return foo { 42, nullptr };
}
Я не чувствую explicit
на самом деле спасает меня от всего, что здесь: в скобках есть два аргумента, так что это, очевидно, не преобразование, а тип foo
уже появляется в подписи, поэтому я хотел бы сэкономить, когда я чувствую, что это излишне. std::tuple
страдает от этой проблемы (хотя заводы, как std::make_tuple
немного облегчить эту боль).
Давайте отдельно выразим строительство а также преобразование ограничения:
// New trait that describes e.g.
// []() -> T { return { std::declval<Args>()... }; }
template<typename T, typename... Args>
struct is_perfectly_convertible_from: std::is_constructible<T, Args...> {};
template<typename T, typename U>
struct is_perfectly_convertible_from: std::is_convertible<U, T> {};
// New constructible trait that will take care that as a constraint it
// doesn't overlap with the trait above for the purposes of SFINAE
template<typename T, typename U>
struct is_perfectly_constructible
: And<
std::is_constructible<T, U>
, Not<std::is_convertible<U, T>>
> {};
Использование:
struct foo {
// General constructor
template<
typename... Args
, EnableIf< is_perfectly_convertible_from<foo_impl, Args...> >...
>
foo(Args&&... args);
// Special unary, non-convertible case
template<
typename Arg
, EnableIf< is_perfectly_constructible<foo_impl, Arg> >...
>
explicit foo(Arg&& arg);
};
Выгода: Строительство и переоборудование foo_impl
В настоящее время необходимы и достаточные условия для строительства и преобразования foo
, То есть std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value
а также std::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value
оба держатся (почти).
Drawback? foo f { 0, 1, 2, 3, 4 };
не работает, если foo_impl
например, std::vector<int>
потому что ограничение с точки зрения конструкции стиля std::vector<int> v(0, 1, 2, 3, 4);
, Можно добавить дополнительную перегрузку std::initializer_list<T>
это ограничено std::is_convertible<std::initializer_list<T>, foo_impl>
(оставлено как упражнение для читателя), или даже перегрузка std::initializer_list<T>, Ts&&...
(ограничение также оставлено в качестве упражнения для читателя — но помните, что «преобразование» из более чем одного аргумента не является конструкцией!). Обратите внимание, что нам не нужно изменять is_perfectly_convertible_from
чтобы избежать совпадения.
Более послушные среди нас также сделают все возможное, чтобы различать узкий конверсии против другого вида конверсий.
Вы можете положить Args
внутри более сложных выражений и расширить это как expression(Args)...
, Следовательно
!std::is_same<Args, typename std::add_lvalue_reference<foo>::type>::value...
Даст вам разделенный запятыми список is_same
за каждый аргумент. Вы можете использовать это как аргументы шаблона для шаблона, комбинируя значения соответственно, давая вам что-то вроде следующего.
template<bool... Args> struct and_;
template<bool A, bool... Args>
struct and_<A, Args...>{
static constexpr bool value = A && and_<Args...>::value;
};
template<bool A>
struct and_<A>{
static constexpr bool value = A;
};
//...
template <typename... Args,
typename Dummy = typename std::enable_if<
and_<!std::is_same<Args,
typename std::add_lvalue_reference<foo>::type
>::value...>::value
>::type
>
foo(Args&&... args) : impl(std::forward<Args>(args)...)
{
std::cout << "uref" << std::endl;
}
Я не совсем уверен, как именно вы хотите ограничить аргументы. Поэтому я не уверен, будет ли это делать то, что вы хотите, но вы должны быть в состоянии использовать этот принцип.