Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clang fails to expand parameter pack in std::function instantiation

The snippet of code compiled with std=c++17 as the only compiler flag ...

  • ... compiles successfully with GCC 9.1. Godbolt
  • ... issues a compiler error with Clang 8.0.0 (error below snippet). Godbolt

Question: is this a bug in the Clang compiler or is GCC wrong to accept this code or is something else at play?

#include <functional>
#include <tuple>

template <typename... Ts>
struct Foo
{
    template <typename T>
    using Int = int;

    // Function that accepts as many 'int' as there are template parameters
    using Function = std::function< void(Int<Ts>...) >;

    // Tuple of as many 'int' as there are template parameters
    using Tuple = std::tuple< Int<Ts>... >;

    auto bar(Function f)
    {
        std::apply(f, Tuple{}); // Error with Clang 8.0.0
    }
};

int main()
{
    auto foo = Foo<char, bool, double>{};
    foo.bar([](int, int, int){});
}

What strikes me as odd is that Clang's error indicates it successfully aliased Tuple as std::tuple<int, int, int> but it wrongly aliases Function as just std::function<void(int)>, with only one instead of three arguments.

In file included from <source>:2:
In file included from /opt/compiler-explorer/gcc-8.3.0/lib/gcc/x86_64-linux-gnu/8.3.0/../../../../include/c++/8.3.0/functional:54:
/opt/compiler-explorer/gcc-8.3.0/lib/gcc/x86_64-linux-gnu/8.3.0/../../../../include/c++/8.3.0/tuple:1678:14: error: no matching function for call to '__invoke'
      return std::__invoke(std::forward<_Fn>(__f),
             ^~~~~~~~~~~~~
/opt/compiler-explorer/gcc-8.3.0/lib/gcc/x86_64-linux-gnu/8.3.0/../../../../include/c++/8.3.0/tuple:1687:19: note: in instantiation of function template specialization 'std::__apply_impl<std::function<void (int)> &, std::tuple<int, int, int>, 0, 1, 2>' requested here
      return std::__apply_impl(std::forward<_Fn>(__f),
                  ^
<source>:19:14: note: in instantiation of function template specialization 'std::apply<std::function<void (int)> &, std::tuple<int, int, int> >' requested here
        std::apply(f, Tuple{}); // Error
             ^
<source>:26:9: note: in instantiation of member function 'Foo<char, bool, double>::bar' requested here
    foo.bar([](int, int, int){});

Additional Research

As other users in the comments already pointed out, making the Int template alias dependent on the type T fixes the issue:

template <typename T>
using Int = std::conditional_t<true, int, T>;

Something else I found out, just referring to the Function type from the outside also makes it work as expected/desired:

int main()
{
    auto f = Foo<char, bool, double>::Function{};
    f = [](int, int, int){};
}
like image 305
Maarten Bamelis Avatar asked Jul 17 '19 16:07

Maarten Bamelis


Video Answer


1 Answers

TL;DR: it's a clang bug, but there's also a bug in the standard.

First be aware that in C++, templates are handled in two steps:

  1. Constructs that are not dependent on the template parameters are built when the enclosing template is defined.
  2. Constructs that are dependent are built when the enclosing template is instantiated.

Now, it seems that clang treats std::function< void(Int<Ts>...) > as a non-dependent type, with the following reasoning:

  1. Int<Ts> is a non-dependent type (correct).
  2. Thus, pack expansion containing Int<Ts> (i.e. Int<Ts>...) is also a non-dependent "type" (?).
  3. Because all components of void(Int<Ts>...) are non-dependent, it is a non-dependent type (obviously incorrect).
  4. Because the name std::function is non-dependent and the template argument void(Int<Ts>...) is a non-dependent non-pack-expansion type, std::function< void(Int<Ts>...) > is a non-dependent type.

(Note that the "non-pack-expansion" check makes it different from the Tuple case.)

Therefore, when Foo is defined, the type name Function is treated as naming a non-dependent type, and is immediately built, and pack expansion (which happens during instantiation) is not accounted. Consequently, all uses of Function is replaced with the "desugared" type std::function< void(int) >.

Furthermore, clang has the notion of instantiation-dependent, which means the construct is not dependent, but it still somehow involves the template parameters (e.g. the construct is only valid for some parameters). std::function< void(Int<Ts>...) > is treated as a instantiation-dependent type, so when the template is instantiated, clang still performs substitution for using Function = std::function< void(Int<Ts>...) >. As a result, Function gets the correct type, but this does not propagate to uses of Function in the definition of Foo.


Now heres the bug in the standard.

Whether a type is dependent is defined in [temp.dep.type]:

A type is dependent if it is

  • a template parameter,
  • a member of an unknown specialization,
  • a nested class or enumeration that is a dependent member of the current instantiation,
  • a cv-qualified type where the cv-unqualified type is dependent,
  • a compound type constructed from any dependent type,
  • an array type whose element type is dependent or whose bound (if any) is value-dependent,
  • a function type whose exception specification is value-dependent,
  • denoted by a simple-template-id in which either the template name is a template parameter or any of the template arguments is a dependent type or an expression that is type-dependent or value-dependent or is a pack expansion [ Note: This includes an injected-class-name of a class template used without a template-argument-list. — end note ] , or
  • denoted by decltype(expression), where expression is type-dependent.

Note that it doesn't say that a function type whose parameter list contains pack expansions is a dependent type, only that "a compound type constructed from any dependent type" and "a function type whose exception specification is value-dependent" are dependent. Neither is helpful here.

like image 75
cpplearner Avatar answered Oct 05 '22 03:10

cpplearner