SFINAE 可以干扰偏序吗?

Can SFINAE interfere with partial ordering?

我正在开发一个库,该库使用名为 PETE 的相当古老的 C++ 表达式模板 (ET) 引擎。 (我试图找到它的源代码 link 以便我可以引用它,但我只找到了关于它的文章。)

快速概述:使用 ET,C++ 编译器使用运算符中缀形式 (+,-,*,/) 从表达式构建表示表达式及其操作的 C++ 类型。 PETE 方法的核心是 ForEach class 模板,用于稍后解析和评估表达式。

我想做的是提供一个专门的 ForEach,当它的参数满足特定条件时使用它。我正在尝试使用部分专业化和 enable_if 的使用,但编译器抱怨 'ambiguous partial specialization'.

如果需要,我很乐意 post 代码的其他部分,但我会坚持使用有问题的直接 class 模板(注意:我添加了 Enable参数,以便使以后的特化 select 能够使用 enable_if。注意 2:为了更短 post,我不包括该方法的实现):

template<class Expr, class FTag, class CTag, class Enable = void>
struct ForEach
{
  typedef typename LeafFunctor<Expr, FTag>::Type_t Type_t;
  inline static
  Type_t apply(const Expr &expr, const FTag &f, const CTag &)
  {
    // empty
  }
};

然后是第一个部分专业化(也是标准 PETE 的一部分)。这就是后来所谓的数字“1”:

// 1
template<class Op, class A, class B, class FTag, class CTag>
struct ForEach<BinaryNode<Op, A, B>, FTag, CTag >
{
  typedef typename ForEach<A, FTag, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, FTag, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, Op, CTag>::Type_t Type_t;
  inline static
  Type_t apply(const BinaryNode<Op, A, B> &expr, const FTag &f,
           const CTag &c) 
  {
    // default implementation for BinaryNode
  }
};

这是我在编译器抱怨的地方附加的部分专业化。它实际上抱怨数字“2”与数字“1”不明确:

// A
template<class A, class B, class CTag>
struct ForEach<BinaryNode<OpMultiply, A, B>, ViewSpinLeaf, CTag , enable_if_t< ! EvalToSpinMatrix<A>::value > >
{
  typedef typename ForEach<A, ViewSpinLeaf, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, ViewSpinLeaf, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, OpMultiply, CTag>::Type_t Type_t;
  inline static
  Type_t apply(const BinaryNode<OpMultiply, A, B> &expr, const ViewSpinLeaf &f,
           const CTag &c)
  {
    // default implementation for BinaryNode (this is the same as above)
  }
};


// 2
template<class A, class B, class CTag>
struct ForEach<BinaryNode<OpMultiply, A, B>, ViewSpinLeaf, CTag , enable_if_t< EvalToSpinMatrix<A>::value > >
{
  typedef typename ForEach<A, ViewSpinLeaf, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, ViewSpinLeaf, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, OpMultiply, CTag>::Type_t Type_t;

  inline static
  Type_t apply(const BinaryNode<OpMultiply, A, B> &expr, const ViewSpinLeaf &f, const CTag &c) 
  {
    // special implementation for when EvalToSpinMatrix<A>::value is true 
  }
};

编译错误如下(注意:为了提高可读性,我重新格式化了它)

  ambiguous template instantiation for ‘struct ForEach<BinaryNode<OpMultiply, Vector<double>, Vector<double> >, ViewSpinLeaf, OpCombine, void>’

  candidate '1':
    candidates are: template<class Op, class A, class B, class FTag, class CTag> struct ForEach<BinaryNode<Op, A, B>, FTag, CTag> ;
Op = OpMultiply;
A = Vector<double>;
B = Vector<double>;
FTag = ViewSpinLeaf;
CTag = OpCombine;

  candidate '2':
note:   template<class A, class B, class CTag> struct ForEach<BinaryNode<OpMultiply, T1, T2>, ViewSpinLeaf, CTag, typename std::enable_if<EvalToSpinMatrix<A>::value, void>::type>;
A = Vector<double>;
B = Vector<double>;
CTag = OpCombine;

根据我的理解,该标准适用所谓的 'partial ordering',它表示部分专业化比另一个专业化更专业,如果它至少与另一个专业化一样,但反之则不然。应用于此示例表示:

数字 2 至少与数字 1 一样专业,因为对于每个参数集(对于数字 2)我都可以找到一个匹配的集合(对于数字 1)。但是数字 1 至少不像数字 2 那样专业。如果我将 FTag 设置为 ViewSpinLeaf 以外的任何值,那么数字 2 将无法匹配。因此,2 号更专业。所以,我不明白为什么编译器不这么看。

作为第二次测试,我删除了专业化 'A'(带有负值 enable_if 的那个)并从专业化“2”中删除了 enable_if_t 位。这编译得很好,这意味着数字 '2' 中的所有其他 statements/typedefs 都可以工作。然而,这不是我所需要的,因为此代码路径随后适用于所有 BinaryNode<OpMultiply,..> 而不仅仅是特定情况。

以防万一。我使用的编译器是 Ubuntu 上的 g++ 9.3,启用了标准 C++14。

编辑:正如评论中所建议的那样,BinaryNode<Op,..>BinaryNode<OpMultiply,..> 之间可能存在歧义。我将数字“2”更改为以下内容:

// 2
template<class Op, class A, class B, class CTag>
struct ForEach<BinaryNode<Op, A, B>, ViewSpinLeaf, CTag , enable_if_t< EvalToSpinMatrix<A>::value > >
{
  typedef typename ForEach<A, ViewSpinLeaf, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, ViewSpinLeaf, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, Op, CTag>::Type_t Type_t;

  inline static
  Type_t apply(const BinaryNode<OpMultiply, A, B> &expr, const ViewSpinLeaf &f, const CTag &c) 
  {
  }
  
  inline static
  Type_t apply(const BinaryNode<Op, A, B> &expr, const ViewSpinLeaf &f, const CTag &c)
  {
  }
};

现在只有 FTag 更专业。编译器抱怨同样的歧义:

note: candidates are: ‘template<class Op, class A, class B, class FTag, class CTag> struct ForEach<BinaryNode<Op, A, B>, FTag, CTag>;
Op = OpMultiply;
A = Vector<double>;
B = Vector<double>;
FTag = ViewSpinLeaf;
CTag = OpCombine;

‘template<class Op, class A, class B, class CTag> struct ForEach<BinaryNode<Op, A, B>, ViewSpinLeaf, CTag, typename std::enable_if<EvalToSpinMatrix<A>::value, void>::type>;
Op = OpMultiply;
A = Vector<double>;
B = Vector<double>;
CTag = OpCombine;

数字“2”显然更专业。

EDIT2:添加一个最小的复制器。有一个 #if 0 如果像这样保留程序编译并采用默认代码路径。但是,当使用 #if 1 打开部分专业化时,会再现歧义。

#include<type_traits>
#include<iostream>

using namespace std;


template<class T>        class Vector {};

struct ViewSpinLeaf {};
struct OpCombine {};

template<class LeafType, class LeafTag> struct LeafFunctor {};
template<class A, class B, class Op, class Tag> struct Combine2 {};


template<class T1, class T2, class Op>
struct BinaryReturn {
  typedef T1 Type_t;
};


template<class T>
struct LeafFunctor<Vector<T>, ViewSpinLeaf>
{
  typedef T Type_t;
  inline static
  Type_t apply(const Vector<T> & s, const ViewSpinLeaf& v)
  {
    return Type_t();
  }
};


template<class A,class B,class Op>
struct Combine2<A, B, Op, OpCombine>
{
  typedef typename BinaryReturn<A, B, Op>::Type_t Type_t;
  inline static
  Type_t combine(const A& a, const B& b, const Op& op, const OpCombine& do_not_use)
  {
    return op(a, b);
  }
};

struct OpMultiply
{
  template<class T1, class T2>
  inline typename BinaryReturn<T1, T2, OpMultiply >::Type_t
  operator()(const T1 &a, const T2 &b) const
  {
    return (a * b);
  }
};


template<class Op, class Left, class Right>
class BinaryNode
{
public:
  BinaryNode(const Op &o, const Left &l, const Right &r) : op_m(o), left_m(l), right_m(r) {}

private:
  Op    op_m;
  Left  left_m;
  Right right_m;
};





template<class Expr, class FTag, class CTag, class Enable = void >
struct ForEach
{
  typedef typename LeafFunctor<Expr, FTag>::Type_t Type_t;
  inline static
  Type_t apply(const Expr &expr, const FTag &f, const CTag &)
  {
    return LeafFunctor<Expr, FTag>::apply(expr, f);
  }
};



template<class Expr, class FTag, class CTag>
inline typename ForEach<Expr,FTag,CTag>::Type_t
forEach(const Expr &e, const FTag &f, const CTag &c)
{
  return ForEach<Expr, FTag, CTag>::apply(e, f, c);
}


template<class Op, class A, class B, class FTag, class CTag>
struct ForEach<BinaryNode<Op, A, B>, FTag, CTag >
{
  typedef typename ForEach<A, FTag, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, FTag, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, Op, CTag>::Type_t Type_t;
  inline static
  Type_t apply(const BinaryNode<Op, A, B> &expr, const FTag &f,
           const CTag &c) 
  {
    std::cout << "I don't want to be here. " << std::endl;
    return Type_t();
  }
};






#if 0

template<class A>
struct EvalToSpinMatrix
{
  constexpr static bool value = false;
};

template<>
struct EvalToSpinMatrix<Vector<double> >
{
  constexpr static bool value = true;
};



template<class A, class B, class CTag>
struct ForEach<BinaryNode<OpMultiply, A, B>, ViewSpinLeaf, CTag , enable_if_t< EvalToSpinMatrix<A>::value > >
{
  typedef typename ForEach<A, ViewSpinLeaf, CTag>::Type_t TypeA_t;
  typedef typename ForEach<B, ViewSpinLeaf, CTag>::Type_t TypeB_t;
  typedef typename Combine2<TypeA_t, TypeB_t, OpMultiply, CTag>::Type_t Type_t;

  inline static
  Type_t apply(const BinaryNode<OpMultiply, A, B> &expr, const ViewSpinLeaf &f, const CTag &c) 
  {
    std::cout << "I want to get here. " << std::endl;
    return Type_t();
  }
};
#endif



int main(int argc, char **argv)
{
  OpMultiply op;
  Vector<double> left;
  Vector<double> right;

  BinaryNode< OpMultiply , Vector<double> , Vector<double> > rhs(op,left,right);
  
  forEach( rhs , ViewSpinLeaf() , OpCombine() );
}

我应该说 select 使用基于 EvalToSpinMatrix 特征的 enable_if 开关的偏特化很重要。显然在实际应用中这个特性是比较复杂的。很好,它在这个简单版本中重现了歧义。

一段时间后我我有一个答案。

首先,我将代码简化到最低限度,删除了重现错误不需要的任何内容。这一切都归结为为什么部分特化在下面是模棱两可的(我很抱歉更改了各个位的名称,但取消膨胀你的代码并不是一件容易的事,至少对我来说):

#include <utility>

template<typename T, typename = void>
struct A {};

template<typename T, typename U>
struct A<std::pair<T,U>> {};

template<typename U>
struct A<std::pair<int,U>, std::enable_if_t<std::is_same_v<int,U>>> {};

int main() {
  A<std::pair<int, int>> x;
}

看起来很像第二个偏特化比第一个更特化,但实际上并非如此。

让我们转到 部分排序 部分 on cppreference 并阅读所有内容:

Informally "A is more specialized than B" means "A accepts a subset of the types that B accepts".

Formally, to establish more-specialized-than relationship between partial specializations, each is first converted to a fictitious function template as follows:

  • the first function template has the same template parameters as the first partial specialization and has just one function parameter, whose type is a class template specialization with all the template arguments from the first partial specialization
  • [the same as above, but s/first/second/g].

The function templates are then ranked as if for function template overloading.

接下来是一个有趣的例子。

在简化代码的情况下,这意味着与第一个特化对应的虚构函数模板具有签名

template<typename T, typename U>
void f(A<std::pair<T,U>>);

而对应于第二个专业化的那个有签名

template<typename U>
void f(A<std::pair<int,U>, std::enable_if_t<std::is_same_v<int,U>>>) {}

这是函数模板的两个不同的重载。因此,问题已通过上面第二个 link 中描述的规则转移到它们中的哪一个是首选。

老实说,在这一点上我有点迷茫,所以我问了,答案是即使第二个重载将std::pair的第一个模板参数固定为int,第一个重载是将 A 的第二个模板参数固定为 void,因此其中 none 比另一个更专业。 std::enable_if/std::enable_if_t 不会改变这种情况,因为它被用作类型 (void,因为我们没有将第二个模板参数传递给 std::enable_if/std::enable_if_t) 的函数参数,而不是模板类型参数。