6

I am trying to implement a function template ovl such that ovl<Foo, Bar>(f) will return the overload of f taking (Foo, Bar), and very surprised with what happens with my naïve solution:

template <class... Args, class Ret>
constexpr auto ovl(Ret (*const f)(std::type_identity_t<Args>...)) { return f; }

void foo();
void foo(int);
void foo(int, int);

int main() {
    ovl<int>(foo)(0);
}
prog.cc:26:5: fatal error: no matching function for call to 'ovl'
    ovl<int>(foo)(0);
    ^~~~~~~~
prog.cc:6:16: note: candidate template ignored: couldn't infer template argument 'Ret'
constexpr auto ovl(Ret (*const f)(std::type_identity_t<Args>...)) { return f; }
               ^

The same error appears with GCC and Clang. What's more, it actually works when enumerating possible arities myself:


template <class Ret>
constexpr auto ovl(Ret (*const f)()) { return f; }

template <class Arg0, class Ret>
constexpr auto ovl(Ret (*const f)(std::type_identity_t<Arg0>)) { return f; }

template <class Arg0, class Arg1, class Ret>
constexpr auto ovl(Ret (*const f)(std::type_identity_t<Arg0>, std::type_identity_t<Arg1>)) { return f; }

// ... ad nauseam.

Wandbox demo

Interestingly, keeping Args... but hardcoding the return type works as well:

template <class... Args>
constexpr auto ovl(void (*const f)(std::type_identity_t<Args>...)) { return f; }

It seems like partial explicit arguments are ignored when they are provided to a parameter pack, but why? And how can I ensure that they are considered when trying to disambiguate the function pointer?


Note: I have found the following workaround which bakes Args... first before deducing Ret, but am still interested in an answer as this is quite clunky.

template <class... Args>
struct ovl_t {
    template <class Ret>
    constexpr auto operator()(Ret (*const f)(Args...)) const { return f; }
};

template <class... Args>
constexpr ovl_t<Args...> ovl;
Quentin
  • 62,093
  • 7
  • 131
  • 191
  • 1
    Not a duplicate by any stretch, but [parameter packs can always be expanded, and so are never "really" given explicitly](https://stackoverflow.com/questions/45318502/why-is-template-parameter-pack-used-in-a-function-argument-type-as-its-template). – StoryTeller - Unslander Monica Dec 08 '20 at 19:20
  • I suspect a proper answer here might get into language-lawyer territory. It may help with attention to tag this question with [tag:language-lawyer] – Human-Compiler Dec 08 '20 at 20:10
  • Can you elaborate on the clunky-ness? Maybe there's some other workarounds. – Passer By Dec 09 '20 at 00:42
  • @StoryTeller-UnslanderMonica I'm afraid that is, if not a duplicate, at least the same cause... dammit. – Quentin Dec 09 '20 at 10:07
  • @PasserBy my complete use cas has additional overloads and variants for member functions (`const`, non-`const`, qualified) and varargs, and whipping out a whole class template and variable template for each case feels a tad unwieldly, if effective -- particularly since I feel like I'm one quirk away from a working function-based implementation. – Quentin Dec 09 '20 at 10:10
  • @PasserBy - My hunch would be that they all fail the trial deduction. I think the "tail end" of the pack is never really deduced as empty for either of them. – StoryTeller - Unslander Monica Dec 09 '20 at 12:33
  • @StoryTeller-UnslanderMonica I was swinging between thinking it's a bug and not. [MSVC is schizophrenic](https://godbolt.org/z/Kefvcc) on this one. – Passer By Dec 09 '20 at 12:38

1 Answers1

1

I believe this is a compiler bug.

As @StoryTeller - Unslander Monica mentioned [temp.arg.explicit]

Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments.

Which means even though we supplied an int argument, the compiler will attempt to deduce more arguments for Args.

However [temp.deduct.call]

If the argument is an overload set (not containing function templates), trial argument deduction is attempted using each of the members of the set. If deduction succeeds for only one of the overload set members, that member is used as the argument value for the deduction. If deduction succeeds for more than one member of the overload set the parameter is treated as a non-deduced context.

The trial deductions are

template<typename... Args, typename Ret>
void trial(Ret(*)(std::type_identity_t<Args>...));

trial<int>((void(*)())foo);         // fails
trial<int>((void(*)(int))foo);      // succeeds
trial<int>((void(*)(int, int))foo); // fails, trailing Args is non-deduced context

Implying that only the void(int) member is used as the argument value, which will succeed in deducing Ret = void and Args = {int}.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • I'm not sold on the "succeeds" assertion. The pack parameter is in a non-deduced context for every argument from the overload set. So why should the empty pack be deducible? – StoryTeller - Unslander Monica Dec 09 '20 at 12:55
  • @StoryTeller-UnslanderMonica I am too impatient, hopefully this time I don't mess up. I believe it's [\[temp.deduct.type\]/10](https://timsong-cpp.github.io/cppwp/temp.deduct.type#10.sentence-4). Otherwise, `void f(int, std::vector...)` won't be able to be called as `f(42)`, which is... weird. – Passer By Dec 09 '20 at 13:21