Ответ 1
Вот несколько способов написания шаблона конструктора с ограниченным ограничением, в порядке возрастания сложности и соответствующего возрастающего порядка многофункциональности и уменьшения порядка количества исправлений.
Эта конкретная форма 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!
Недостаток: участвует во всех других разрешениях перегрузки
Ограничить конструктивное выражение
Так как конструктор имеет моральные последствия построения a 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
сохраняются (почти).
Недостаток? 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
, чтобы избежать совпадения.
Более угодливые среди нас также будут стараться различать узкие преобразования против других преобразований.