以下是编写适当约束的构造函数模板的不同方法,按照复杂性的递增顺序以及相应的特征丰富性递增顺序和陷阱数量递减顺序。
将使用这种特殊形式的 EnableIf,但这是一个实现细节,不会改变此处概述的技术的本质。还假设存在And
和Not
别名来组合不同的元计算。egAnd<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
from的道德效果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
都(几乎)成立。
退税? 如果is egfoo f { 0, 1, 2, 3, 4 };
则不起作用,因为约束是根据 style 的构造。可以添加一个受约束的进一步超载承受(作为练习留给读者),甚至是超载承受(约束也作为练习留给读者——但请记住,从多个“转换”论证不是构造!)。请注意,我们不需要修改以避免重叠。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
我们当中更谄媚的人也会确保将狭隘的皈依与其他类型的皈依区分开来。