15

In my C++ travels I've come across the following idiom (for example here in Abseil) for ensuring that a templated function can't have template arguments explicitly specified, so they aren't part of the guaranteed API and are free to change without breaking anybody:

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

And it does seem to work:

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'

I understand at an intuitive level why this works, but I'm wondering how to make it precise. What section(s) of the standard guarantee that there is no way to explicitly specify template arguments for this template?

I'm surprised by clang's excellent error message here; it's like it recognizes the idiom.

jacobsa
  • 5,719
  • 1
  • 28
  • 60
  • 1
    [A non-type template parameter must be a converted constant expression](http://eel.is/c++draft/temp.arg.nontype#2). – user2407038 Jun 28 '21 at 05:48
  • 1
    @user2407038: True, but on first glance you'd say that `typename T` is a type parameter, and `char` names a type. The real question here is why a template that takes N non-type parameters and 1 type parameter, when instantiated with 1 type, still sets N to >=1. – MSalters Jun 28 '21 at 06:57
  • It does not work for class template as the parameter pack must be at the end. – nhatnq Jun 28 '21 at 11:07
  • There are no ways to "end" a variadic (even by passing other template parameter "type" as non-type variadic, followed by type, or template template parameter). – Jarod42 Jun 28 '21 at 13:09
  • I'm wondering about the claim, though. `template void AcceptSomeReference(const char&)` appears to be an explicit instantiation. See 13.9.2 Explicit instantiation. But that's an instantiation, and does not actually _call_ the function. – MSalters Jun 28 '21 at 15:24
  • Yeah, sorry if I misused some precise terminology. I mean to say "what guarantees it can't be called with explicit template arguments". @user2407038 seems to have a convincing citation. – jacobsa Jun 29 '21 at 09:12
  • Ah but I also agree with @MSalters; I guess we also need a citation for "there's no way to end a template argument pack". To be honest mostly I'm surprised by clang's excellent error message; it's like it recognizes this idiom. – jacobsa Jun 29 '21 at 09:15
  • @jacobsa: when you tag it "language-lawyer", then precise terminology matters. – MSalters Jun 29 '21 at 09:19

2 Answers2

8

The main reason that a construct of the form

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

prohibits you from specifying the template argument for T explicitly is that for all template parameters that follow a template parameter pack either the compiler must be able to deduce the corresponding arguments from the function arguments (or they must have a default argument) [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.

As pointed out by @DavisHerring this paragraph alone does not necessarily mean that it must be deducted. But: Finding the matching template arguments is performed in several steps: First the explicitly specified template argument list will be considered [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

This means whether a template argument can be deducted or not depends not only on the function template declaration but also on the steps before the template argument deduction is applied such as the consideration of the explicitly specified template argument list. A template arguments following a template parameter pack can't be specified explicitly as then it would not be participating in template argument deduction ([temp.arg.explicit]/7) and therefore would also not be deducted (which would violate [temp.param]/14).

When explicitly specifying the template arguments the compiler will therefore match them "greedily" to the first parameter pack (as for the following arguments either default values should be available or they should be deductible from function arguments! Counter-intuitively this is even the case if the template arguments are type and non-type parameters!). So there is no way of fully explicitly specifying the template argument list of a function with a signature

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

If it is called with

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

all the types would be attributed to the template parameter pack and never to T. So in the example above ExplicitArgumentBarrier = {int,void,int} and T will be deducted from the argument 8.0 to double.


To make things even worse you could use a reference as template parameter

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

It isn't impossible to explicitly specify template arguments for such a barrier but reference template parameters are very restrictive and it is very unlikely to happen by accident as they have to respect [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;
}

This way by adding this variadic template of unlikely template arguments as the first template argument you can hold somebody off from specifying any template parameters at all. Whatever the users will try to put will very likely fail compiling. Without an explicit template argument list ExplicitArgumentBarrier will be auto-deducted to have a length of zero [temp.arg.explicit]/4/Note1.


Additional comment to @DavisHerring

Allowing somebody to explicitly specify the template arguments following a variadic template would lead to ambiguities and break existing rules. Consider the following function:

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

What would the template arguments in the call func<double>(8.0) be? Ts = {}, U = double or Ts = {double}, U = double? The only way around this ambiguity would be to allow the user to only specify all the template arguments explicitly (and not only some). But then this leads again to problems with default arguments:

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

A call to func2<double,double>(8.0) is again ambiguous (Ts = {}, U = double, V = double or Ts = {double}, U = double, V = int). Now you would have to ban default template arguments in this context to remove this ambiguity!

2b-t
  • 2,414
  • 1
  • 10
  • 18
  • The fact that they must be deducible doesn’t by itself imply that they must be deduced (as suggested by the alternative of it having a default template argument). The rest of your analysis is correct; it just doesn’t follow from the quoted rule. – Davis Herring Jun 30 '21 at 02:07
  • @DavisHerring Thanks for your comment. I will add a reference to [temp.deduct] later on. – 2b-t Jun 30 '21 at 07:49
  • You can pass an argument indirectly, by casting to a function pointer type, I suspect. `static_cast(&func)` will "pass" T=int, I suspect. – Johannes Schaub - litb Jun 30 '21 at 18:22
  • This answer is great, but I'm interested in the answer to Davis Herring's objection. What seals up that hole? – jacobsa Jul 01 '21 at 07:42
  • @jacobsa I see but. I think [temp.deduct.type/9](https://eel.is/c++draft/temp.fct.spec#temp.deduct.type-9) should clarify this objection from the standpoint of the standard. I just added it to my explanation. – 2b-t Jul 01 '21 at 23:11
  • @DavisHerring Sorry for adding it only now but [temp.deduct.type/9](https://eel.is/c++draft/temp.fct.spec#temp.deduct.type-9) should be the answer to your objection if I am not mistaken. – 2b-t Jul 01 '21 at 23:14
  • @2b-t: The process of matching explicit template arguments to template parameters is not deduction, though. – Davis Herring Jul 02 '21 at 01:27
  • @DavisHerring No, the deduction follows later on but - correct me if I am mistaken - this line states that for an (explicitly given) template argument list that contains a non-trailing parameter pack the entire explicitly given template argument list is a [non-deduced context](https://en.cppreference.com/w/cpp/language/template_argument_deduction): None of it will be assumed to be deductible but will be instead matched greedily to the non-trailing parameter pack. Then it will go on and perform deduction. – 2b-t Jul 02 '21 at 06:37
  • That seems to perfectly address it (although I'd be curious if @DavisHerring has some other concern), thanks! – jacobsa Jul 03 '21 at 04:35
  • @jacobsa: I don’t want to debate this forever, but I still don’t think that’s relevant: that paragraph concerns function parameter types that themselves contain template argument packs, as in `template void f(std::tuple non_deduced,std::tuple deduced);`, not the parameter pack itself. – Davis Herring Jul 03 '21 at 05:02
  • @DavisHerring Thanks for pointing that out, I did not notice that. But anyways then also the note in [temp.arg.explicit-7](https://eel.is/c++draft/temp.arg.explicit#7) should be sufficient: `Note: Template parameters do not participate in template argument deduction if they are explicitly specified.`. – 2b-t Jul 03 '21 at 20:43
  • @2b-t: Isn’t the whole question which template *parameters* are explicitly specified by the given template arguments? – Davis Herring Jul 03 '21 at 21:23
  • @DavisHerring Yes, it is. Please correct me if I am misquoting you: Your objection was that [temp.param]/14 only states that template parameter following a parameter pack shall be deductible from the function arguments (or be defaulted) but not that they necessarily must be deducted this way. But the way of getting the list of template arguments has actually three main steps: First comes the explicitly specified argument list, then comes the template argument deduction and only then default values are considered. This is described in [temp.deduct.general]/2-5. – 2b-t Jul 03 '21 at 22:30
  • @DavisHerring Now what is deductible not only depends on the function template declaration but also on the steps that happen before the template argument deduction, such as the consideration of the explicitly given template argument list. And as mentioned in [temp.arg.explicit]/7: Explicitly specified template arguments do not participate in the deduction process that follows. This means that if you specify the template arguments explicitly these arguments are not deductible. – 2b-t Jul 03 '21 at 22:31
  • So my understanding of the standard is that explicitly specified template arguments are not deductible as they do not participate in template argument deduction. – 2b-t Jul 03 '21 at 22:37
  • @2b-t: Being deducible is a property of the declaration, independent of any usage, which might or might not explicitly specify them (and thus prevent any deduction that might be possible for them). We know that you can’t so specify this kind, but the question is how we know that. We can’t appeal to the behavior of any particular call for that. – Davis Herring Jul 04 '21 at 00:01
  • @DavisHerring I don't want to annoy you, I am really interested in this but could you supply a resource for your claim? The standard does not use deductible at all, that was my (possibly erroneous) paraphrasation. As said if you have a look at the standard and what it takes to know if you can deduct it or not there are steps like consideration of the explicit template argument list beforehand. Take the example `template ` and then a call with ``, then you will see that the given template id is valid. True -> the explicit argument list will be considered. – 2b-t Jul 04 '21 at 00:30
  • Then you have substitutions and type adjustments (nothing happens in this case). And only then you have the template argument deduction. So if it can be deducted clearly depends on the steps that took you there. If I am missing something here please let me know. – 2b-t Jul 04 '21 at 00:40
  • @2b-t: I already wrote an answer; I don’t think the standard actually says what happens here. As such, I don’t have much of a “resource”: proving a negative is always difficult. As for “deducible”, I meant only the “can be deduced” from [temp.param]/14, which is a property of the template and not a *template-id* naming it (note the “parameter-type-list”). – Davis Herring Jul 04 '21 at 01:07
  • @DavisHerring I understand and I appreciate your critique. I agree with you in the sense that I could not find an explicit paragraph dealing with it neither but as pointed out in my argument I think this does not mean that this is not covered by the standard. I feel like this discussion is not going to reach a consensus so I'd like to thank for your time and hope to see you around on another post. If jacobsa feels like my answer leaves doubts or is incomplete I invite him to unaccept the answer. – 2b-t Jul 04 '21 at 01:26
  • Any idea how this interacts with [temp.res]/6.3, which says a program is ill-formed if "every valid specialization of a variadic template requires an empty template parameter pack"? Seems like maybe that applies here? – jacobsa Mar 14 '22 at 20:31
  • @jacobsa Sorry for the late reply; I have been largely absent from StackOverflow lately. I do not think this applies here, you can specialize this template as outlined in my post but it is quite selective with its arguments. This section applies to situations like [this](https://cppquiz.org/quiz/question/228?result=UD&answer=&did_answer=Answer), where a variadic template would only result in valid C++ code without any template arguments. In this case the standard does not guarantee that the resulting program will be meaningful - the behavior is undefined. – 2b-t Apr 22 '22 at 23:21
  • [temp.res/6.3](http://eel.is/c++draft/temp.res#general-6.3) should also apply to something like `template struct S { std::tuple t; };`. A tuple takes only types and not non-types (such as `int`) as template arguments but one may instantiate an empty tuple, resulting in said case that "every valid specialization of a variadic template requires an empty template parameter pack". – 2b-t Apr 22 '22 at 23:54
0

The standard is very vague about several aspects of template argument usage, including which template parameter “corresponds” to each argument. [temp.arg]/1 merely says

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

which can be read as saying that any and all template arguments (past those for any prior template parameters) not just can but do correspond to the template parameter pack.

Note that it is still possible to specify template arguments for the pack explicitly—the point is that it’s useless, since you can’t for the following (i.e., meaningful) template parameters. The use of a type like int& is just to make it less likely that you would succeed in doing so accidentally and think it accomplished something.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • I'm pretty sure that the basic rule is that template parameters are matched with template arguments based on the order in which they appear, just like function arguments. (C++ has no named arguments, unlike e.g. Python). It's the unspecified "zero or more" which causes the problem. Remember that this is a Standard defining implementations. A compiler that matches zero arguments for the pack in the given example would appear to be conformant, yet compile the example. – MSalters Jun 29 '21 at 09:23