C++ 标准的哪一部分阻止显式指定此模板的参数?

Which part of the C++ standard prevents explicitly specifying this template's arguments?

在我的 C++ 旅行中,我遇到了以下习语(例如 Abseil 中的 here),用于确保模板化函数不能显式指定模板参数,因此它们不是保证 API 并且可以在不破坏任何人的情况下自由更改:

template <int&... ExplicitArgumentBarrier, typename T>
void AcceptSomeReference(const T&);

它似乎确实有效:

foo.cc:5:3: error: no matching function for call to 'AcceptSomeReference'
  AcceptSomeReference<char>('a');
  ^~~~~~~~~~~~~~~~~~~~~~~~~
foo.cc:2:6: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'ExplicitArgumentBarrier'

我从直觉上理解为什么这样做有效,但我想知道如何使其精确。 标准的哪些部分保证无法明确指定此模板的模板参数?

我对这里 clang 的出色错误消息感到惊讶;好像认识成语一样。

该标准对模板参数使用的几个方面非常模糊,包括每个参数“对应”哪个模板参数。 [temp.arg]/1 只是说

When the parameter declared by the template is a template parameter pack, it will correspond to zero or more template-arguments.

这可以理解为任何和所有模板参数(超过任何先前模板参数的参数)不仅仅是 cando对应模板参数包

请注意,仍然可以显式地为 pack 指定模板参数——关键是它没有用,因为你不能为以下 (,有意义)模板参数。使用像 int& 这样的类型只是为了减少您意外成功并认为它完成了某些事情的可能性。

表格构造的主要原因

 template <int&... ExplicitArgumentBarrier, typename T>
 void AcceptSomeReference(T const&);

禁止您为 T 明确指定模板参数是因为对于 所有跟在模板参数包 之后的模板参数,编译器 必须能够从函数arguments中推导出相应的参数(或者它们必须有一个默认参数)[temp.param]/14:

A template parameter pack of a function template shall not be followed by another template parameter unless that template parameter can be deduced from the parameter-type-list ([dcl.fct]) of the function template or has a default argument ([temp.deduct]). A template parameter of a deduction guide template ([temp.deduct.guide]) that does not have a default argument shall be deducible from the parameter-type-list of the deduction guide template.

正如@DavisHerring 所指出的,仅此一段并不一定意味着必须扣除。但是:查找匹配的模板参数是分几个步骤执行的:首先,显式指定的模板参数列表将被视为 [temp.deduct.general]/2, only later on the template type deduction will be performed and finally default arguments are considered [temp.deduct.general]/5. [temp.arg.explicit]/7 states in this context

Note 3: Template parameters do not participate in template argument deduction if they are explicitly specified

这意味着模板参数是否可以被推导,不仅取决于函数模板声明,还取决于应用模板参数推导之前的步骤,例如考虑显式指定的模板参数列表。无法明确指定模板参数包之后的模板参数,因为它不会参与模板参数推导 ([temp.arg.explicit]/7),因此也不会被推导(这将违反 [temp.param]/14).

因此,当显式指定模板参数时,编译器会将它们“贪婪地”匹配到第一个参数包(至于以下参数,默认值应该可用或者它们应该从函数参数中扣除!这与直觉相反即使模板参数是 ! 也是如此)。所以没有办法完全显式指定带有签名

的函数的模板参数列表
template <typename... ExplicitArgumentBarrier, typename T>
void AcceptSomeReference(const T&);

如果用

调用
AcceptSomeReference<int,void,int>(8.0);

所有类型都将归因于模板参数包,永远不会归因于 T。所以在上面的例子中 ExplicitArgumentBarrier = {int,void,int}T 将从参数 8.0 中扣除到 double.


更糟糕的是,您可以使用引用作为模板参数

template <int&... ExplicitArgumentBarrier, typename T>
void AcceptSomeReference(const T&);

为此类障碍显式指定模板参数并非不可能,但参考模板参数非常严格并且不太可能偶然发生,因为它们必须遵守[temp.arg.nontype]/2 (constexpr for non-types) and [temp.arg.nontype]/3 (even more restrictive for references!): You would need some sort of static variable with the correct cv-qualifier (e.g. something like static constexpr int x or static int const x in the following example would not work either!) like in the following code snippet (Try it here!):

template <int&... ExplicitArgumentBarrier, typename T>
void AcceptSomeReference(const T& t) {
  std::cout << t << std::endl;
  return;
}

static int x = 93;
int main() {
  AcceptSomeReference<x>(29.1);
  return EXIT_SUCCESS;
}

通过这种方式,通过将不太可能的模板参数的可变参数模板添加为第一个模板参数,您可以完全阻止某些人指定任何模板参数。无论用户尝试输入什么,都很可能无法编译。如果没有明确的模板参数列表,ExplicitArgumentBarrier 将被自动扣除为零长度 [temp.arg.explicit]/4/Note1.


@DavisHerring 的补充评论

允许某人在可变参数模板之后明确指定模板参数会导致歧义并破坏现有规则。考虑以下函数:

template <typename... Ts, typename U>
void func(U u);

调用 func<double>(8.0) 中的模板参数是什么? Ts = {}, U = double 还是 Ts = {double}, U = double?解决这种歧义的唯一方法是 允许用户仅明确指定所有模板参数 (而不仅仅是一些)。但这又会导致默认参数出现问题:

template <typename... Ts, typename U, typename V = int>
void func2(U u);

func2<double,double>(8.0) 的调用再次出现歧义(Ts = {}, U = double, V = doubleTs = {double}, U = double, V = int)。现在你必须在这个上下文中禁止默认模板参数来消除这种歧义!