如何使用SFINAE来选择构造函数?

34
在模板元编程中,可以使用SFINAE(Substitution Failure Is Not An Error)在返回类型上选择特定的模板成员函数。
template<int N> struct A {
  int sum() const noexcept
  { return _sum<N-1>(); }
private:
  int _data[N];
  template<int I> typename std::enable_if< I,int>::type _sum() const noexcept
  { return _sum<I-1>() + _data[I]; }
  template<int I> typename std::enable_if<!I,int>::type _sum() const noexcept
  { return _data[I]; }
};

然而,这在构造函数上不起作用。假设我想声明构造函数。
template<int N> struct A {
   /* ... */
   template<int otherN>
   explicit(A<otherN> const&); // only sensible if otherN >= N
};

但是不允许 otherN < N
所以,SFINAE 可以在这里使用吗?我只对允许自动模板参数推导的解决方案感兴趣,这样可以
A<4> a4{};
A<5> a5{};
A<6> a6{a4};  // doesn't compile
A<3> a3{a5};  // compiles and automatically finds the correct constructor

注意:这只是一个非常简化的例子,其中SFINAE可能过于复杂,而static_assert可能足够。然而,我想知道我是否可以使用SFINAE。

8
我认为我们真的应该有一个核心语言功能来替换这种SFINAE滥用,我希望如此...... - sellibitze
请修改代码以实现您的意图。 - Johannes Schaub - litb
@JohannesSchaub-litb,我不理解你的编辑/评论。A<6> a6{a4};是(打算)调用类似于复制构造函数的函数,我想使用SFINAE。因此,参数必须是一种类型,而不是A<4>::operator()的结果,就像您的A<6> a6(a4())一样。 - Walter
@Walter,问题在于a4和a5是函数。现在已经修复了。 - Johannes Schaub - litb
5个回答

32

您可以向模板添加默认类型参数:

template <int otherN, typename = typename std::enable_if<otherN >= N>::type>
explicit A(A<otherN> const &);

3
请注意,如果您有两个具有不同条件的这样的构造函数,则此方法将无法正常工作。(如果您没有重载函数,您可能应该使用static_assert,但这已经在问题中处理过了。) - R. Martinho Fernandes
1
@KerrekSB:因为第一个typename澄清了enable_if::type必须是一个typename,我认为。 - thiton
1
@KerrekSB:这是可选的?真的吗?好吧,从上下文来看应该很清楚需要跟随一个类型名称,但我不知道C++实际上允许在这里删除第二个typename。你确定吗?也许你是用一个更宽容的编译器尝试的,它实际上没有进行正确的两阶段查找? - sellibitze
1
@R.MartinhoFernandes:不过,静态断言是不同的。 enable_if 的重点是为了获得 std::is_constructible<A<N>, A<M>>::value 的正确行为。 - Kerrek SB
1
请注意,如果您有两个具有不同条件的构造函数,则此代码将无法正常工作。您有什么办法可以解决这个问题吗?我只找到了通过标签分派到第三个构造函数的方法。 - gnzlbg
显示剩余10条评论

23

有许多方法可以触发SFINAE,其中enable_if只是其中之一。 首先:

什么是std::enable_if?

它只是这样的:

template<bool, class T=void> enable_if{ typedef T type; };
template<class T> enable_if<false,T> {};
template<bool b, class T=void> using enable_if_t = typename enable_f<b,T>::type;

这个想法是使typename enable_if<false>::type成为一个错误,从而跳过包含它的任何模板声明。

那么这如何触发函数选择呢?

禁用函数

这个想法是在某些部分使声明出错:

按返回类型

template<class Type>
std::enable_if_t<cond<Type>::value,Return_type> function(Type);

通过实参

template<class Type>
return_type function(Type param, std::enable_if_t<cond<Type>::value,int> =0) 

通过模板参数

template<class Type, 
    std::enable_if_t<cond<Type>::value,int> =0> //note the space between > and =
return_type function(Type param) 

选择函数

您可以使用类似这样的技巧来参数化不同的替代方案:

tempplate<int N> struct ord: ord<N-1>{};
struct ord<0> {};

template<class T, std::enable_if<condition3, int> =0>
retval func(ord<3>, T param) { ... }

template<class T, std::enable_if<condition2, int> =0>
retval func(ord<2>, T param) { ... }

template<class T, std::enable_if<condition1, int> =0>
retval func(ord<1>, T param) { ... }

template<class T> // default one
retval func(ord<0>, T param) { ... }

// THIS WILL BE THE FUCNTION YOU'LL CALL
template<class T>
retval func(T param) { return func(ord<9>{},param); } //any "more than 3 value"

如果满足condition3,则会调用第一个/第二个/第三个/第四个函数,然后是condition2,然后是condition1,最后是没有条件。

其他SFINAE触发器

编写编译时条件可以是显式特化的问题,也可以是未评估表达式成功/失败的问题:

例如:

template<class T, class = void>
struct is_vector: std::false_type {};
template<class X>
struct is_vector<vector<X> >:: std::true_type {};

所以is_vector<int>::value应该为false,但is_vecttor<vector<int> >::value应该为true

或者通过内省来实现,如

template<class T>
struct is_container<class T, class = void>: std::false_type {};

template<class T>
struct is_container<T, decltype(
  std::begin(std::declval<T>()),
  std::end(std::declval<T>()),
  std::size(std::declval<T>()),
  void(0))>: std::true_type {};

这样做,is_container<X>::value将在给定X x时为true,您可以编译std::begin(x)等。

关键是,如果所有子表达式都可编译,则decltype(...)实际上是void(逗号运算符会丢弃先前的表达式)。


甚至可能有许多其他选择。希望在这一切之间,您能找到一些有用的东西。


9
接受的答案对于大多数情况都是不错的,但如果存在具有不同条件的两个这样的构造函数重载,则会失败。我也在寻找这种情况下的解决方法。 是的:接受的解决方案有效,但不适用于两个替代构造函数,例如,
template <int otherN, typename = typename std::enable_if<otherN == 1>::type>
explicit A(A<otherN> const &);

template <int otherN, typename = typename std::enable_if<otherN != 1>::type>
explicit A(A<otherN> const &);

因为,正如此页面所述,

一个常见的错误是声明两个函数模板,它们只在默认模板参数上有所不同。这是非法的,因为默认模板参数不是函数模板签名的一部分,声明具有相同签名的两个不同函数模板是非法的。

如同该页面提出的,您可以通过对值(而不是类型)模板参数的类型应用SFINAE来修改签名,从而解决这个问题。
template <int otherN, typename std::enable_if<otherN == 1, bool>::type = true>
explicit A(A<otherN> const &);

template <int otherN, typename std::enable_if<otherN != 1, bool>::type = true>
explicit A(A<otherN> const &);

template<typename U = T,typename enable_if_t<is_same_v<U,opcode>,bool> =true> constexpr arg(U&&v) : value{v}, name{"opcode"}中,报错信息为error: expected a qualified name after 'typename' - kyb
@kyb - 没有完整的示例很难说出有用的东西,但是也许在 enable_if_tis_same_v 之前加上 std:: 会有所帮助? - max66
我正在使用命名空间std。看起来我应该问一个不同的问题。 - kyb
@kyb - 是的:请提供一个包含完整示例的新问题。 - max66

9
在C++11中,您可以使用默认模板参数:
template <int otherN, class = typename std::enable_if<otherN >= N>::type>
explicit A(A<otherN> const &);

然而,如果您的编译器尚未支持默认模板参数,或者需要多个重载,则可以像这样使用默认函数参数:

template <int otherN>
explicit A(A<otherN> const &, typename std::enable_if<otherN >= N>::type* = 0);

4

使用C++20,您可以使用requires关键字

使用C++20,您可以摆脱SFINAE。

requires关键字是enable_if的简单替代品!

请注意,otherN == N情况是一个特殊情况,因为它会落到默认复制构造函数上,所以如果您想处理它,您需要单独实现它:

template<int N> struct A {
   A() {}    

   // handle the case of otherN == N with copy ctor
   explicit A(A<N> const& other) { /* ... */ }

   // handle the case of otherN > N, see the requires below
   template<int otherN> requires (otherN > N)
   explicit A(A<otherN> const& other) { /* ... */ }

   // handle the case of otherN < N, can add requires or not
   template<int otherN>
   explicit A(A<otherN> const& other) { /* ... */ }
};

requires”子句是一个常量表达式,其评估结果为truefalse,用于决定是否在重载决策中考虑此方法。如果requires子句为真,则此方法比没有requires子句的另一个方法更为特定,因此会被优先考虑。

源代码:https://godbolt.org/z/RD6pcE


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接