(Im) идеальная пересылка с помощью шаблонов

При наличии типа с конструктором шаблона с переменным числом аргументов, который передает аргументы классу реализации, возможно ли ограничить типы, передаваемые с помощью 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

14

Решение

Вот различные способы написания правильно ограниченного шаблона конструктора, в порядке возрастания сложности и соответствующего возрастающего порядка богатства функций и убывающего порядка числа ошибок.

Эта конкретная форма 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 чтобы избежать совпадения.

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

19

Другие решения

Вы можете положить 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;
}

Я не совсем уверен, как именно вы хотите ограничить аргументы. Поэтому я не уверен, будет ли это делать то, что вы хотите, но вы должны быть в состоянии использовать этот принцип.

4

По вопросам рекламы [email protected]