SFINAE 基于 Class 成员 Existence/Absence

SFINAE Based on Class Member Existence/Absence

我对SFINAE有基本的了解,例如enable_if 是如何工作的。我最近遇到了 this answer,我花了一个多小时试图了解它的实际工作原理,但无济于事。

此代码的目标是根据 class 中是否包含特定成员来重载函数。这是复制的代码,它使用 C++11:

template <typename T> struct Model
{
    vector<T> vertices;

    void transform( Matrix m )
    {
        for(auto &&vertex : vertices)
        {
          vertex.pos = m * vertex.pos;
          modifyNormal(vertex, m, special_());
        }
    }

private:

    struct general_ {};
    struct special_ : general_ {};
    template<typename> struct int_ { typedef int type; };

    template<typename Lhs, typename Rhs,
             typename int_<decltype(Lhs::normal)>::type = 0>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, special_) {
       lhs.normal = rhs * lhs.normal;
    }

    template<typename Lhs, typename Rhs>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, general_) {
       // do nothing
    }
};

我这辈子都搞不懂这个机制是如何工作的。具体来说,typename int_<decltype(Lhs::normal)>::type = 0帮我们做了什么,为什么我们需要在这个方法中多一个类型(special_/general_)。

special_/general_ 是用来让编译器区分两种方法的类型 modifyNormal。请注意,不使用第三个参数。一般实现什么也不做,但在特殊情况下它会修改法线。 另请注意,special_ 派生自 general_。这意味着如果未定义 modifyNormal 的特殊版本 (SFINAE),则适用一般情况;如果存在专门版本,则将选择它(更具体)。

现在modifyNormal的定义中有一个开关;如果类型(模板的第一个参数)没有名为 normal 的成员,则模板失败(SFINAE,不要抱怨它,这是 SFINAE 的技巧),它将是 [=12 的另一个定义=] 将适用(一般情况)。如果类型定义了一个名为 normal 的成员,那么模板的第三个参数可以解析为额外的第三个参数模板(int 默认等于 0)。第三个参数对函数没有任何意义,仅用于 SFINAE(模式适用于它)。

why do we need an extra type (special_/general_) in this method

这些仅用于允许 modifyNormal 函数被不同的实现重载的目的。 特别 的地方在于 special_ 使用 IS-A 关系,因为它继承自 general_。此外,transform 函数总是调用采用 special_ 类型的 modifyNormal 重载,请参阅下一部分。

what does typename int_<decltype(Lhs::normal)>::type = 0 help us do

这是一个具有默认值的模板参数。默认值存在,因此 transform 函数不必指定它,这很重要,因为另一个 modifyNormal 函数没有此模板参数。此外,添加此模板参数仅用于调用 SFINAE。

http://en.cppreference.com/w/cpp/language/sfinae

When substituting the deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error.

因此,如果发生故障,采用 special_ 类型的 modifyNormal 函数将从要考虑的重载集中删除。这只会让 modifyNormal 函数采用 general_ 类型,因为 special_ IS-A general_ 类型一切仍然有效。

如果没有发生替换失败,那么将使用使用 special_ 类型的 modifyNormal 函数,因为它是更好的匹配。


注意: general_ 类型是一个 struct 所以默认继承是 public ,允许没有IS-A关系使用 public 关键字。


编辑:

Can you comment on why we use the elaborate typename int_<decltype(Lhs::normal)>::type mechanism in the first place?

如上所述,这用于触发 SFINAE 行为。但是,当您将其分解时,它并不是很详尽。它的核心是要为某种类型 T 实例化 int_ 结构的一个实例,并且它定义了一个 type 数据类型:

int_<T>::type

由于这是在模板中使用的,因此需要添加 typename 关键字,请参阅 When is the “typename” keyword necessary?

typename int_<T>::type

最后,用于实例化 int_ 结构的实际类型是什么?这由 decltype(Lhs::normal) 确定,它报告 Lhs::normal 的类型。如果类型 Lhs 类型有一个 normal 数据成员,那么一切都会成功。但是,如果不是,则表示替换失败,这一点的重要性已在上面进行了解释。

类型 generalspecial 用于强制编译器在尝试解析调用时选择第一个函数(匹配更好)作为第一次尝试。
注意调用是:

modifyNormal(vertex, m, special_());

无论如何,因为general继承自special,两者都有效,如果第一个在类型替换期间失败,将选择第二个。

所以,这就像说 - 让我们试一试,就好像该方法确实存在一样,但请保持冷静,因为我们有一个不执行任何操作的万能回调

为什么会失败?
这就是 int_ 参与游戏的地方。
如果decltype(让我说)报错因为成员方法normalLhs中丢失,int_不能专门化并且替换实际上失败了,但是由于 SFINAE,只要存在另一个可以尝试的替换(并且在我们的例子中存在,该方法以 general 作为参数,那么这个失败就不是错误仍然是原始呼叫的不太精确的匹配。