0

I'm writing a template wrapper function that can be applied to a functions with different number/types of arguments. I have some code that works but I'm trying to change more arguments into template parameters.

The working code:

#include <iostream>

int func0(bool b) { return b ? 1 : 2; }
//There is a few more funcX...

template<typename ...ARGS>
int wrapper(int (*func)(ARGS...), ARGS... args) { return (*func)(args...) * 10; }

int wrappedFunc0(bool b) { return wrapper<bool>(func0, b); }

int main()
{
    std::cout << wrappedFunc0(true) << std::endl;
    return 0;
}

Now I want int (*func)(ARGS...) to also be a template parameter. (It's for performance reasons. I want the pointer to be backed into the wrapper, because the way I'm using it prevents the compiler from optimizing it out.)

Here is what I came up with (The only difference is I've changed the one argument into a template parameter.):

#include <iostream>

int func0(bool b) { return b ? 1 : 2; }
//There is a few more funcX...

template<typename ...ARGS, int (*FUNC)(ARGS...)>
int wrapper(ARGS... args) { return (*FUNC)(args...) * 10; }

int wrappedFunc0(bool b) { return wrapper<bool, func0>(b); }

int main()
{
    std::cout << wrappedFunc0(true) << std::endl;
    return 0;
}

This doesn't compile. It shows:

<source>: In function 'int wrappedFunc0(bool)':
<source>:9:55: error: no matching function for call to 'wrapper<bool, func0>(bool&)'
    9 | int wrappedFunc0(bool b) { return wrapper<bool, func0>(b); }
      |                                   ~~~~~~~~~~~~~~~~~~~~^~~
<source>:7:5: note: candidate: 'template<class ... ARGS, int (* FUNC)(ARGS ...)> int wrapper(ARGS ...)'
    7 | int wrapper(ARGS... args) { return (*FUNC)(args...) * 10; }
      |     ^~~~~~~
<source>:7:5: note:   template argument deduction/substitution failed:
<source>:9:55: error: type/value mismatch at argument 1 in template parameter list for 'template<class ... ARGS, int (* FUNC)(ARGS ...)> int wrapper(ARGS ...)'
    9 | int wrappedFunc0(bool b) { return wrapper<bool, func0>(b); }
      |                                   ~~~~~~~~~~~~~~~~~~~~^~~
<source>:9:55: note:   expected a type, got 'func0'
ASM generation compiler returned: 1
<source>: In function 'int wrappedFunc0(bool)':
<source>:9:55: error: no matching function for call to 'wrapper<bool, func0>(bool&)'
    9 | int wrappedFunc0(bool b) { return wrapper<bool, func0>(b); }
      |                                   ~~~~~~~~~~~~~~~~~~~~^~~
<source>:7:5: note: candidate: 'template<class ... ARGS, int (* FUNC)(ARGS ...)> int wrapper(ARGS ...)'
    7 | int wrapper(ARGS... args) { return (*FUNC)(args...) * 10; }
      |     ^~~~~~~
<source>:7:5: note:   template argument deduction/substitution failed:
<source>:9:55: error: type/value mismatch at argument 1 in template parameter list for 'template<class ... ARGS, int (* FUNC)(ARGS ...)> int wrapper(ARGS ...)'
    9 | int wrappedFunc0(bool b) { return wrapper<bool, func0>(b); }
      |                                   ~~~~~~~~~~~~~~~~~~~~^~~
<source>:9:55: note:   expected a type, got 'func0'
Execution build compiler returned: 1

(link to the compiler explorer)

It looks like a problem with the compiler to me, but GCC and Clang agree on it so maybe it isn't.

Anyway, how can I make this template compile correctly with templated pointer to a function?

EDIT: Addressing the duplicate flag Compilation issue with instantiating function template I think the core of the problem in that question is the same as in mine, however, it lacks a solution that allows passing the pointer to function (not only its type) as a template parameter.

Piotr Siupa
  • 3,929
  • 2
  • 29
  • 65
  • Could you elaborate on the kind of compiler optimization prevented by using e.g. `template int wrapper(F f, Args&&... args) { return f(std::forward(args)...) * 10; } int wrappedFunc0(bool b) { return wrapper(func0, b); }`? I mean sure, it looks like there's a lot of extra instructions, if you don't use any optimization flags, but going with `-O3` this basically seems gone – fabian Apr 16 '22 at 10:13
  • 1
    Does this answer your question? [Compilation issue with instantiating function template](https://stackoverflow.com/questions/29941499/compilation-issue-with-instantiating-function-template) – 康桓瑋 Apr 16 '22 at 10:21
  • @fabian Idn. Probably none. It's not the code I said it would prevent them. Are you suggesting ditching strict type control in template arguments and left it to compilation error when instantiotion goes wrong? I guess it is a solution, although, I'm a little afraid it lefts some space for hidden errors resulting from automatic type conversion. – Piotr Siupa Apr 16 '22 at 10:22
  • @fabian The problems with optimization I mentioned, result from me having the instantiation of the templates in a `cc` file, not a `hh` from which they could be inlined. The compiler wouldn't have all the necessary information to optimize these pointrs out, if they were passed as normal function arguments. – Piotr Siupa Apr 16 '22 at 10:27

2 Answers2

3

This doesn't work because a pack parameter (the one including ...) consumes all remaining arguments. All arguments following it can't be specified explicitly and must be deduced.

Normally you write such wrappers like this:

template <typename F, typename ...P>
int wrapper(F &&func, P &&... params)
{
    return std::forward<F>(func)(std::forward<P>(params)...) * 10;
}

(And if the function is called more than once inside of the wrapper, all calls except the last can't use std::forward.)

This will pass the function by reference, which should be exactly the same as using a function pointer, but I have no reasons to believe that it would stop the compiler from optimizing it.

You can force the function to be encoded in the template argument by passing std::integral_constant<decltype(&func0), func0>{} instead of func0, but again, I don't think it's going to change anything.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • I think my comments under the question adress some of the points in this answer. This is an OK solution, although, I'm always trying to have strict type control if I'm able. Are you saying, it is not possible in this case? – Piotr Siupa Apr 16 '22 at 10:34
  • @NO_NAME You could add `requires std::invocable` (and also replace the manual call with `std::invoke` for consistency). If you mean banning implicit conversions for arguments, that should be possible, it's just not normally done. – HolyBlackCat Apr 16 '22 at 10:36
  • OK, I've read that again and I'm not as confused as the first time. By strict type control I mean the function type should be tied to the `P` so it doesn't allow setting `F` to an unrelated type. (I don't know if this is really that important. Having the wrong type would probably fail the compilation at some point anyway.) However, I'm specifically looking for a solution that allow passing the pointer to a function itself as a template parameter. That's why I tried to change my original, working version of the code. – Piotr Siupa Apr 16 '22 at 11:05
  • @NO_NAME `std::integral_constant` effectively passes the function pointer through the template parameter, as you wanted. – HolyBlackCat Apr 16 '22 at 11:05
  • I cannot figure out how to put `std::integral_constant` into a template parameter but `template` seems to work quite nicely. – Piotr Siupa Apr 16 '22 at 11:22
  • 1
    @NO_NAME `wrapper(std::integral_constant{}, true)`. Since `integral_constant` is an empty structure, it's effectively the same as specifying the template parameter. – HolyBlackCat Apr 16 '22 at 11:27
1

The 2nd snippet is not valid because:

a type parameter pack cannot be expanded in its own parameter clause.

As from [temp.param]/17:

If a template-parameter is a type-parameter with an ellipsis prior to its optional identifier or is a parameter-declaration that declares a pack ([dcl.fct]), then the template-parameter is a template parameter pack. A template parameter pack that is a parameter-declaration whose type contains one or more unexpanded packs is a pack expansion. ... A template parameter pack that is a pack expansion shall not expand a template parameter pack declared in the same template-parameter-list.

So consider the following invalid example:

template<typename... Ts, Ts... vals> struct mytuple {}; //invalid

The above example is invalid because the template type parameter pack Ts cannot be expanded in its own parameter list.

For the same reason, your code example is invalid. For example, a simplified version of your 2nd snippet doesn't compile in msvc.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • Huh, I were wondering if this is valid but I expected the compiler to give an error if it isn't and it doesn't complain at this line at all. – Piotr Siupa Apr 16 '22 at 10:39
  • 1
    I don't think the quote applies here. Both GCC, Clang, and MSVC [allow it](https://gcc.godbolt.org/z/EPMrGvWKE). Note *"A **template parameter** pack that is a pack expansion"* - this expansion is not a template parameter. – HolyBlackCat Apr 16 '22 at 10:40
  • @HolyBlackCat [Simplified version](https://godbolt.org/z/9qbG1d5vr) don't compile in msvc. – Jason Apr 16 '22 at 10:55
  • I don't know if MSVC should be a deciding factor here. Microsoft has a long history of not complying to widely accepted standards and trying to reinvent them. – Piotr Siupa Apr 16 '22 at 11:08
  • @NO_NAME That's why i quoted the standard. I agree that msvc is not the deciding factor. I added the msvc link solely to show that there is reason to be doubtful here. – Jason Apr 16 '22 at 11:21
  • @AnoopRana That error is unrelated to the pack being used in the second parameter, since `template` also causes the same error. The compiler just tells you that the last parameter can neither be deduced nor specified manually. – HolyBlackCat Apr 16 '22 at 11:29