8

If it's possible, I would like to define a macro that would take number of arguments in function template ARGS_COUNT and make a template with all argument types being enumerated and output being common type. For example, if it is defined as

TEMPLATE_COMMON_FLOAT(N) /* ...definition...*/

then

TEMPLATE_COMMON_FLOAT(4)
foo(F0 a, F1 b, F2 c, F3 d) { /*...*/ }

should expand to

template <class F0, class F1, class F2, class F3>
  requires std::is_floating_point_v<F0> && std::is_floating_point_v<F1> && std::is_floating_point_v<F2> && std::is_floating_point_v<F3>
std::common_type_t<F0, F1, F2, F3> foo(F0 a, F1 b, F2 c, F3 d) { /*...*/ }

I want this, since I would like to refer to Fi sometimes, instead of bloating function definition with std::floating_point auto and decltypeing after.

11
  • 11
    why is it not a variadic template? Commented Oct 30 at 12:59
  • 1
    @kusok_kvarka If you just want to constrain the function to have a fixed number of arguments, you can constrain a variadic function template with requires sizeof...(Ts) == 4 Commented Oct 30 at 13:06
  • 1
    if you are actually going for a macro (please don't) I think it would be simpler to aim for TEMPLATE_COMMON_FLOAT(foo,F0,a,F1,b,F2,c,F3,d) { ... } Commented Oct 30 at 13:20
  • 1
    C++26 introduces parameter pack indexing. This should be heady for you strange case. Commented Oct 30 at 15:24
  • 1
    A macro is almost never the right choice - it can be, but it should be you absolute last option. Commented Oct 30 at 15:25

6 Answers 6

10

Using a variadic template should make it possible to have more readable code.

Then, if you want to access the nth argument, you can rely on std::get and the std::tuple machinery

#include <tuple>
#include <iostream>

template <class...Fs>
std::common_type_t<Fs...> foo(Fs&&...fs) {

    // we get the arguments as a tuple.
    auto tup = std::forward_as_tuple(std::forward<Fs>(fs)...);

    // we can access for instance the 4th item (if any)
    static_assert (sizeof...(Fs)>3);
    auto f3 = std::get<3>(tup);

    return f3; // why not ?
}

auto main() -> int
{
    std::cout << foo (0,1,2,3,4) << "\n";
}

Demo

As observed in the comments, you can add a requirement that sets a fixed number of parameters (see here)

If you are mainly interested by retrieving the nth type Fn, you could use some alias that retrieves the required index from a std::tuple defined in the template parameters:

template <class...Fs, typename Ts=std::tuple<Fs...>>
std::common_type_t<Fs...> foo(Fs&&...fs) {
    using F3 = std::tuple_element_t<3,Ts>;
    return {};
}
Sign up to request clarification or add additional context in comments.

1 Comment

Well, it's not really what I wanted... It kinda ruines syntax for a function, I don't want to just pass every time in any function all_Needed_Data thing and then alias its members inside for what they actually represent. Although if nothing better appears, I guess I can use that for some time.
5

If the goal is mainly to process some type checkings on your parameters, you could have the following approach (without macros).

First, you say that you propose some "skeleton" function, e.g.

TEMPLATE_COMMON_FLOAT(4)
foo(F0 a, F1 b, F2 c, F3 d) { return a+b+c+d; }

Instead, you could define a functor foo like this (note that this is more verbose since you have to explicit the template parameters)

template <class F0, class F1, class F2, class F3>
struct foo  {
    auto operator() (F0 a, F1 b, F2 c, F3 d) {  return a+b+c+d; }
};

Then, you can define a structure (say CommonFloatChecker) that encapsulates a generic functor Fct and makes the required checkings at compile time. At runtime, an object of type Fct will be called with the input arguments.

template<template<typename...> typename Fct>
struct CommonFloatChecker  {
    template<typename...Fs>
    requires(std::is_floating_point_v<Fs> && ...)
    std::common_type_t<Fs...> operator() (Fs&&...fs) {
        return Fct<Fs...>{} (std::forward<Fs>(fs)...);
    }
};

You can use it as follows:

auto main() -> int {

    CommonFloatChecker<foo> bar;

    std::cout << bar (0.0,1.0,2.0,3.0) << "\n";
    // bar (0,1,2,3.0);  // KO not all floats
}

The advantage is that you can provide different kinds of checkers that can be applied to your functor foo.

Demo

It is possible to provide a more succinct "skeleton" by using auto for parameters types:

struct foo {
    auto operator() (auto a, auto b, auto c, auto d) {  return a+b+c+d; }
};

and

template<typename Fct>
struct CommonFloatChecker  {
    template<typename...Fs>
    requires(std::is_floating_point_v<Fs> && ...)
    std::common_type_t<Fs...> operator() (Fs&&...fs) {
        return Fct{} (std::forward<Fs>(fs)...);
    }
};

with the drawback that you don't have any more a name Fn for the parameters types.

Demo

Following Toby Speight's note, one can skip the requires clause by directly using the std::floating_point concept:

template<typename Fct>
struct CommonFloatChecker  {
    template<std::floating_point...Fs>
    std::common_type_t<Fs...> operator() (Fs&&...fs) {
        return Fct{} (std::forward<Fs>(fs)...);
    }
};

Demo

6 Comments

auto return type might be unacceptable if the body looks something like if (a < b) return c; return d + (a - b); (different return statements might not use all of the arguments, so can have different types)
I like that there is at least some way to do it) I guess that's what I would use, if it would still be defined only in functions somehow, it is pretty hard to understand for me. But for people who are okay with this, I will accept this as a solution, thanks.
@Artyer, that's a good point. OTOH, it is somewhat the purpose of the checker to detect such a case (or at least provide a better error message) (see)
I'm talking something like foo(double{}, double{}, double{}, float{}): godbolt.org/z/s1Eq3fT9q
@kusok_kvarka, you have also the option to use a lamba function like this which may look more familiar to you.
@Artyer, ok got it. Maybe one can provide the return type as a template parameter of foo like this
3

This can be reached w/o macros, w/ using variadic templates.

#include <type_traits>

template <class... Args>
  requires(std::is_floating_point_v<Args> && ...)
std::common_type_t<Args...> foo(Args&&... args) { /*...*/ }

int main(int argc, char** argv) {
  foo(1.0, 2.0);
}

If a fixed number number of parameters is needed, the concept can be extended.

#include <type_traits>

template <size_t N, class... Args>
  requires(sizeof...(Args) == N && (std::is_floating_point_v<Args> && ...))
std::common_type_t<Args...> foo(Args&&... args) {
  return (args + ...);
}

int main(int argc, char** argv) {
  foo<2>(1.0, 2.0);
}

If individual arguments are required to be accessed.

#include <tuple>
#include <type_traits>

template <size_t N, class... Args>
  requires(sizeof...(Args) == N && (std::is_floating_point_v<Args> && ...))
std::common_type_t<Args...> foo(Args&&... args) {
  const auto arg = std::forward_as_tuple(std::forward<Args>(args)...);
  return [&arg]<size_t... Is>(std::index_sequence<Is...>) {
    return (... + std::get<Is>(arg));
  }(std::make_index_sequence<N>{});
}

int main(int argc, char** argv) {
  foo<2>(1.0, 2.0);
  return 0;
}

1 Comment

What's the difference between the second and the third?
3

Since you write out foo(F0 a, F1 b, F2 c, F3 d), you probably aren't computing the number passed to TEMPLATE_COMMON_FLOAT, and you have a maximum number of arguments.

So you can easily implement this as a series of macros instead:

#define TEMPLATE_COMMON_FLOAT_1 \
template<std::floating_point F0> \
std::common_type_t<F0>
#define TEMPLATE_COMMON_FLOAT_2 \
template<std::floating_point F0, std::floating_point F1> \
std::common_type_t<F0, F1>
#define TEMPLATE_COMMON_FLOAT_3 \
template<std::floating_point F0, std::floating_point F1, std::floating_point F2> \
std::common_type_t<F0, F1, F2>
#define TEMPLATE_COMMON_FLOAT_4 \
template<std::floating_point F0, std::floating_point F1, std::floating_point F2, std::floating_point F3> \
std::common_type_t<F0, F1, F2, F3>
// Up to how many times you need for your code


// And if you *really* want TEMPLATE_COMMON_FLOAT(N)
#define TEMPLATE_COMMON_FLOAT(N) TEMPLATE_COMMON_FLOAT_ ## N
// Or if you have a large number of these, you can write them in terms of the previous version
// XTEMPLATE_COMMON_ARGS_ ## N(B, L) expands to B L0, B L1, B L2, ..., B L(N-1)
#define XTEMPLATE_COMMON_ARGS_1(BEFORE, LETTER) BEFORE LETTER ## 0
#define XTEMPLATE_COMMON_ARGS_2(BEFORE, LETTER) XTEMPLATE_COMMON_ARGS_1(BEFORE, LETTER), BEFORE LETTER ## 1
#define XTEMPLATE_COMMON_ARGS_3(BEFORE, LETTER) XTEMPLATE_COMMON_ARGS_2(BEFORE, LETTER), BEFORE LETTER ## 2
#define XTEMPLATE_COMMON_ARGS_4(BEFORE, LETTER) XTEMPLATE_COMMON_ARGS_3(BEFORE, LETTER), BEFORE LETTER ## 3
#define XTEMPLATE_COMMON_ARGS_5(BEFORE, LETTER) XTEMPLATE_COMMON_ARGS_4(BEFORE, LETTER), BEFORE LETTER ## 4
// ...

#define TEMPLATE_COMMON_FLOAT(N) \
template<XTEMPLATE_COMMON_ARGS_ ## N(std::floating_point, F)> \
std::common_type_t<XTEMPLATE_COMMON_ARGS_ ## N(, F)>

However, a simpler (and no preprocessor) solution is a variadic template:

template<std::floating_point... Fs> requires(sizeof...(Fs) == 4)
std::common_type_t<Fs...> foo(Fs... fs) {
    auto [ a, b, c, d ] = std::tie(fs...);  // Can access by name now
    /* ... */
}

And it's not actually that difficult to write with an abbreviated function template for floating point types:

auto foo(std::floating_point auto a, std::floating_point auto b, std::floating_point auto c, std::floating_point auto d) -> decltype(a+b+c+d) {
    // ...
}

// Or if you can't accept float being promoted to double
auto foo(std::floating_point auto a, std::floating_point auto b, std::floating_point auto c, std::floating_point auto d) -> decltype(0?a:0?b:0?c:d) {
    // ...
}

Comments

2

This is a bit of a frame challenge: Probably you don't need a macro for N parameters as the number of parameters is usually very limited.

I have seen such an approach on an older version of the gtest framework which ran on an older C++ version where variadic templates were not yet supported.

The very simple solution of the framework is/was to provide a limited amount of copies of the same macro for 0 to 10 parameters (e.g. named MOCK_METHOD0 to MOCK_METHOD10) where each defining code is relatively short and built upon other macros.

It is very unusual to require more than 10 parameters on a method and if it happens then it is easy and straigt forward to make another copy which can support e.g. 15 parameters.

I agree, a solution with variadic arguments is more elegant (and such restrictions were the reason to support variadic arguments in later C++ standards), but the copying solution is simple and backward compatible.

Comments

0

If your boss asks you to build this macro and do not accept any excuse, try Boost.Preprocessor library:

#include <boost/preprocessor.hpp>

#define TEMPLATE_COMMON_FLOAT_R1(z, i, _) \
    BOOST_PP_COMMA_IF(i) class BOOST_PP_CAT(F, i)
#define TEMPLATE_COMMON_FLOAT_R2(z, i, _) \
    BOOST_PP_IF(i, &&, requires) std::is_floating_point_v<BOOST_PP_CAT(F, i)>
#define TEMPLATE_COMMON_FLOAT_R3(z, i, _) \
    BOOST_PP_COMMA_IF(i) BOOST_PP_CAT(F, i)

#define TEMPLATE_COMMON_FLOAT(n) \
    template < BOOST_PP_REPEAT(n, TEMPLATE_COMMON_FLOAT_R1, _) > \
    BOOST_PP_REPEAT(n, TEMPLATE_COMMON_FLOAT_R2, _) \
    std::common_type_t<BOOST_PP_REPEAT(n, TEMPLATE_COMMON_FLOAT_R3, _)>

TEMPLATE_COMMON_FLOAT(4)
foo(F0 a, F1 b, F2 c, F3 d) { /*...*/ }

which expands to (see online compiler result):

template < class F0 , class F1 , class F2 , class F3 > requires std::is_floating_point_v<F0> && std::is_floating_point_v<F1> && std::is_floating_point_v<F2> && std::is_floating_point_v<F3> std::common_type_t< F0 , F1 , F2 , F3>
foo(F0 a, F1 b, F2 c, F3 d) { }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.