9

G++ 和 Clang++ 同意以下代码段不是有效的 C++:

template<int dim, int rank>
struct Tensor {};

template<int dim>
double InnerProduct(Tensor<dim, 1> const &, Tensor<dim, 1> const &)
  { return 0.0; }

template<int dim>
double DoubleInnerProduct(Tensor<dim, 2> const &, Tensor<dim, 2> const &)
  { return 0.0; }

template<int dim, int rank>
class Field
{
private:
  static double Dot(Tensor<dim, rank> const &u, Tensor<dim, rank> const &v) requires (rank == 1)
    { return InnerProduct(u, v); }

  static double Dot(Tensor<dim, rank> const &u, Tensor<dim, rank> const &v) requires (rank == 2)
    { return DoubleInnerProduct(u, v); }
};

template class Field<2, 1>;
template class Field<2, 2>;

错误消息指出,即使是具有未满足约束的函数也会被实例化:

error: no matching function for call to ‘DoubleInnerProduct(const Tensor<2, 1>&, const Tensor<2, 1>&)’
   22 |     { return DoubleInnerProduct(u, v); }

我可以通过多种方式使其工作(例如,声明Dot为具有等于rank应用约束的默认参数的模板),但我希望它能够工作。

一般来说,我是否应该假设不能显式实例化具有取决于其模板参数的约束的成员函数的模板类?

4

2 回答 2

2

显式类模板实例化定义也是在实例化点定义的那些成员的显式实例化定义

考虑以下简化示例:

template<int rank>
struct A {};

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

[temp.explicit]/11状态 [强调我的]:

命名类模板特化的显式实例化也是其先前未显式特化的每个成员(不包括从基类继承的成员和模板成员)的同类(声明或定义)的显式实例化包含显式实例化的翻译单元,前提是该成员的关联约束(如果有)由显式实例化的模板参数([temp.constr.decl],[temp.constr.constr])满足,除非是如下面所描述的。[...]

这意味着一个显式的实例化定义,它只命名一个类模板特化Field,比如说

template struct Field<1>;

也将导致dot满足约束表达式的重载的显式实例化定义 requires (rank == 1),但不适用于具有约束表达式的重载requires (rank == 2)。但是,除了下面描述的部分将我们引向[temp.explicit]/12,其中指出 [强调我的]:

命名类模板特化的显式实例化定义显式实例化类模板特化,并且是仅在实例化点已定义的那些成员的显式实例化定义。

这意味着,对于上面的简化示例(随后是 的显式实例化定义Field<1>,如上),上面的段落指示了两个 dot重载的显式实例化定义,因为两者都已在 的显式实例化定义处定义Field<1>。然而,这意味着违反 ODR,因为将有两个定义Field<1>::void dot(A<1>)

// Not OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

template struct Field<1>;

int main() {}

在 Clang 上产生以下错误:

error: definition with same mangled name '_ZN5FieldILi1EE3dotE1AILi1EE' as  another definition
       void dot(A<rank>) requires (rank == 2) { }

请注意,我们可以为类模板的给定特化提供明确的实例化定义,特别是类模板的dot非模板成员Field,并且 GCC 和 Clang 将很乐意接受它,这表明在显式实例化重载,受约束的函数:

// OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

template void Field<1>::dot(A<1>);

int main() {}

但如上所述,当它们根据上面的 [temp.explicit]/12 引用隐式给出显式实例化定义时,则不会,因为这似乎为两个成员提供了单独的实例化定义(不考虑约束表达式),因此违反了 ODR .

类模板特化的显式实例化定义与特化的非模板成员函数之间编译器的不同行为有些特殊,但可能的区别在于后一种情况, [temp.constr.constr]/2适用[强调我的]

[...]重载解决需要满足对函数和函数模板的约束。


如果我们只声明但不定义第二个重载,它将不会被实例化为 的显式实例化定义(即[temp.explicit]/12不适用)的一部分Field<1>,我们将不再有违反 ODR:

// OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2);
};

template struct Field<1>;

int main() {}

现在,为什么这对于隐式实例化没有失败?

根据[temp.inst]/3 [强调我的]:

类模板特化的隐式实例化导致

(3.1)非删除类成员函数、成员类、范围成员枚举、静态数据成员、成员模板和朋友的声明的隐式实例化,但不是定义的隐式实例化;和 [...]

使得 Clang 和 GCC 都接受以下示例:

template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

int main() { 
    Field<1> f{};
    (void)f;
}

其中,根据[temp.inst]/4dot重载将不会被实例化,因为在Field<1>需要其定义存在的上下文中未引用特化。

然而,最后,我们可能会注意到类模板的dot静态成员函数的隐式实例化Field将尊重约束表达式,并实例化满足rank特定类模板特化的非模板参数约束的重载:

#include <iostream>

template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { std::cout << "1"; }
    void dot(A<rank>) requires (rank == 2) { std::cout << "2"; } 
};

int main() { 
    Field<1>{}.dot(A<1>{}); // "1"
}

如上所述,这可能由[temp.constr.constr]/2 控制

于 2020-06-30T15:32:13.353 回答
1

这是 GCC 和 Clang 的 c++20 实验实现的错误。

这被报告为 GCC错误 #77595

在 c++20 段落 [temp.explicit]/11 中:

命名类模板特化的显式实例化也是其先前未显式特化的每个成员(不包括从基类继承的成员和模板成员)的同类(声明或定义)的显式实例化包含显式实例化的翻译单元,前提是该成员的关联约束(如果有)由显式实例化的模板参数([temp.constr.decl],[temp.constr.constr])满足,除非是下面介绍。[...]

根据这个 c++20 附录“只要满足该成员的相关约束(如果有的话)”意味着只有两个Dot重载中的一个应该被显式实例化。

c++17 标准中的粗体子句“除非如下所述”。它确实适用于第一个子句“命名类模板特化的显式实例化也是其每个成员的同类(声明或定义)的显式实例化”。这个异常的解释在 c++20 中没有改变。可能委员会忽略了这一点,从语法上讲,将例外应用于 c++20 附录可能是正确的。

下面是这一段的c++17版本:

命名类模板特化的显式实例化也是其先前未显式特化的每个成员(不包括从基类继承的成员和模板成员)的同类(声明或定义)的显式实例化包含显式实例化的翻译单元,除非下面描述。

在这个较短的句子中,意思很清楚。

于 2020-07-01T12:26:15.363 回答