3

I want to have a function with unlimited number of parameters but I want also make sure that those are all pointers of the same type. Something like this:

void myFunc(float value, MyClass* ....)
{
    // take all pointers of type MyClass and call function `f` like this: a->(value);
    // store pointer in a vector like: vector.push_back(a);
}

Can I achieve this in C++?

Narek
  • 38,779
  • 79
  • 233
  • 389

2 Answers2

8
void myFunc(float value, std::initializer_list<MyClass*> il){
  for(auto* p:il)
    p->f(value);
}

no heap/free store allocation will occur.

Use is myFunc(3.14, {ptr1,ptr2,ptr3});

If you really hate {} you can forward to the above with an unrestricted template. At the point of forwarding there will be type checking. SFINAE can be used to type check earlier.

template<class...MyClasses>
void myFunc2( float value, MyClasses*... ps) {
  myFunc( value, {ps...} );
}

(possibly with name changes)

Alternatively, full SFINAE based solutions can be done, with direct calling of p->f, bit that is like using a bazooka to deal with litter. Sure, the litter will be gone, but it was still a bad idea.

initializer lists are designed for efficient bundling of parameters of identical type.

Now, a good question to ask about your MyClass* request is ... why do you care? If the passed in parameters are compatible with ->f(3.14f), why not just call ->f(3.14f)? This is why practical problems are better problems to ask about, rather than abstract ones: the best solution to a problem varies with practical issues.

The bazooka solution looks like the follows.

First, a small template metaprogramming library:

// Better name for the type:
template<bool b>
using bool_t = std::integral_constant<bool, b>;
// bundle of types:
template<class...>struct types{using type=types;};

// takes a test, and a types<?...> of things to test against it:
template<template<class...>class Z, class args>
struct test_lists:std::true_type{};
template<template<class...>class Z, class...Ts, class...Us>
struct test_lists<Z,types<types<Ts...>,Us...>>:bool_t<
  Z<Ts...>{} && test_lists<Z, types<Us...>>{}
>{};

// takes 0 or more types<?...> and concatenates them:
template<class...types>
struct concat_types;
template<class...types>
using concat_types_t=typename concat_types<types...>::type;
template<>
struct concat_types<>:types<>{};
template<class...Ts>
struct concat_types<types<Ts...>>:types<Ts...>{};
template<class...T0s,class...T1s, class...more>
struct concat_types<types<T0s...>,types<T1s...>,more...>:
  concat_types_t< types<T0s...,T1s...>, more... >
{};

// takes a template Z and and arg, and produces a template
// equal to Z<Arg, ?...>:
template<template<class...>class Z, class Arg>
struct bind_1st {
    template<class...Ts>
    using apply=Z<Arg,Ts...>;
};

// takes a template Z and a types<?...> and produces
// types< Z<?>... >:
template<template<class...>class Z, class types>
struct map;
template<template<class...>class Z, class types>
using map_t=typename map<Z,types>::type;
template<template<class...>class Z, class...Ts>
struct map<Z,types<Ts...>>:types<Z<Ts>...>{};

// builds a cross product of zero or more types<?...>:
template<class...types0>
struct cross_types;
template<class...types>
using cross_types_t=typename cross_types<types...>::type;

// valid degenerate cases:
template<class...Ts>
struct cross_types<types<>,Ts...>:types<>{};
template<>
struct cross_types<>:types<types<>>{};

// meat of cross_types:
template<class T0, class...T0s, class...Us>
struct cross_types<types<T0,T0s...>, Us...>:
  concat_types_t<
    map_t< bind_1st< concat_types_t, types<T0> >::template apply, cross_types_t<Us...> >,
    cross_types_t< types<T0s...>, Us... >
  >
{};

// takes a test Z, and a sequence of types<?...> args
// tests the cross product of the contents of the args:
template<template<class...>class Z, class...Args>
struct test_cross : test_lists<Z, cross_types_t<Args...>> {};

everything above this point is generic metaprogramming code. You can do the next part more directly, but the generic metaprogramming code above can be used in other similar problems, and it does make the later stuff "clearer".

// a toy MyClass type to test against:
struct MyClass {
    void f(float x){
        std::cout << x << '\n';
    }
};

// Simple SFINAE test that the types passed in are exactly
// pointers to MyClass:
template<class...Ts>
std::enable_if_t<
  test_cross<std::is_same, types<MyClass>, types<Ts...>>{}
>
myFunc( float x, Ts*... p ) {
  using discard=int[];
  (void)discard{0,((
    p->f(x)
  ),void(),0)...};
}

note that std::is_base_of might be a better choice than is_same.

The core is here:

  test_cross<std::is_same, types<MyClass>, types<Ts...>>{}

this evaluates std::is_same<A,B> for every pair of <MyClass, Ts>.

A far easier way to do it would be a template that took a bunch of bool... and did an && on them, together with std::is_same<MyClass, Ts>{}.... But I like writing metaprogramming libraries, and doing n-way cross product tests with brevity is a more interesting problem.

live example

Cross product based off of this stack overflow answer in python

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Can't we achieve `myFunc(3.14, ptr1, ptr2, ptr3);`? Is it technically possible? – Narek Apr 15 '15 at 15:40
  • @narek yes: the easy way is to simply forward the extra args to the above function. The hard way is the bazooka comment. Clear? – Yakk - Adam Nevraumont Apr 15 '15 at 15:42
  • Bazooka part is clear, implementation - not. :) And what you mean by bazooka? Implementation is hard, or it is more inefficient (slow, more memory)? – Narek Apr 15 '15 at 15:44
  • @narek more complex to compile? Harder to implement with little return? Harder for people to read? – Yakk - Adam Nevraumont Apr 15 '15 at 16:04
  • @Narek I loaded the bazooka, and fired it. I made little attempt to make it the smallest bazooka that would solve the problem, but instead amused myself over lunch. -- now with the bug in `cross` fixed. – Yakk - Adam Nevraumont Apr 15 '15 at 17:43
  • +1 for the nuclear bomb... forget bazooka.. I do have to ask, is there a difference to my example below? – Nim Apr 16 '15 at 10:06
  • @nim yes: sfinae early error instead of a static assert late error. Basically the error you get is 'no overload matches', as if you passed the wrong type. – Yakk - Adam Nevraumont Apr 16 '15 at 11:46
1

You could do the following, then you can stick to your existing syntax..

#include <iostream>
#include <type_traits>

using namespace std;

struct V
{
    int a;
    int b;
};

// Little test function to ensure that it's the type we're expecting..
template <typename T>
int test(T*)
{
    static_assert(is_same<T, V>::value, "Must only be V");
    return 0;
}

template <typename ...T>
void foo(int a, T*... args)
{
    // Use expansion to test all the parameters, this will generate
    // an error if the wrong type is passed in
    auto v = { (test(args), 0)... };
    (void) v; // result discarded..
}

int main()
{
    V a, b, c, d, e;
    int f;
    foo(10, &a, &b, &c, &d, &e, &f); // this will fail
    foo(10, &a, &b, &c, &d, &e); // this will compile
}

Basically use the a parameter pack with a static_assert to force the type to be the same...

Nim
  • 33,299
  • 2
  • 62
  • 101