Search code examples
c++c++17language-lawyervariadic-templatesc++20

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


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.


Solution

  • 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!